關於flutter的背景、體系結構、橫向對比等,建議閱讀淘寶的一篇文章,大比拼|下一代高效能跨平臺UI渲染引擎,人家是真的厲害。
這裡就不多貼這些巨集觀的簡介了。本文主要從客戶端開發的角度看三個小點:執行緒、非同步、宣告式UI,都是Flutter跟正常的客戶端開發有一定區別的地方。
執行緒模型
執行緒模型
執行緒模型這塊,大多數文章對初學者來講都有點不清不楚的,這裡詳細總結一下。
首先,在上層flutter APP裡,我們用dart語言開發,這個層面,是沒有執行緒的概念的,取而代之的是dart提供的類似執行緒的isolate。isolate簡單來講就是個受限制的執行緒,isolate之間只能通過一種叫port的訊息機制通訊,不能共享記憶體。除此之外跟執行緒是一樣的。
dart vm預設提供了一個root isolate,有耗時操作需要執行時,可以new出新的isolate執行。
Flutter engine這個層面,有四個Runner各司其職,這裡的Runner其實就是執行緒,不過這四個Runner是由Engine和Native之間的那個嵌入層去賦值的,engine層只會使用這四個Runner,不會建立新的執行緒。預設地,Platform Runner和Native的主執行緒是同一個執行緒。
回頭看dart的root isolate,它跟engine層的UI Runner是繫結的,即,它們兩個是同一個執行緒。
整體看一下,會發現一些特別的東西。對Flutter App來講,root isolate基本上可以理解為主執行緒,同時它也是UI執行緒。但是,它不是Native層面的主執行緒,在Native看來,它只是個子執行緒。
dart非同步程式設計
callback
對非同步程式設計而言,客戶端開發最熟悉的可能是callback語法,當然很多時候也會使用delegate。dart的callback語法如下:
Timer.run(() => print('hi!'));
複製程式碼
不過雖然dart也可以用callback,但是更多的時候,會使用Future/async/await這套語法來執行非同步任務。
Future/async/await
Future<Response> dateRequest() async {
String url = 'https://www.baidu.com';
Client client = Client();
Future<Response> response = client.get(requestURL);
return response;
}
Future<String> loadData() async {
Response response = await dataRequest();
return response.body;
}
複製程式碼
簡單看一下這個小例子,client.get()
是個非同步的網路請求,它可以直接返回一個Future<Response>
的物件,這個名字很有意思,它的意思是,我以後會給你個Response
型別的物件的,但是現在,只是個空頭支票(Future
)。
之後,可以使用await關鍵字加上這個Future
,當前呼叫就會停在這裡,直到這個Future
物件返回才會繼續向下執行。基本原理是,把當前上下文存到堆記憶體;當Future
返回時,會產生一個event進入eventloop(基本上是個語言都有這麼個玩意兒,可以參考Dart與訊息迴圈機制),這個event會觸發進入之前的上下文繼續執行。
可以看到,這裡的寫法很像同步的寫法,但是它是不會阻塞當前執行緒的,原理上面已經簡單解釋了。目前,async/await這種非同步語法,是公認的非同步語法的最佳方案。前端和安卓的kotlin已經比較廣泛地使用了,而iOS還沒跟得上時代。
單執行緒語言 & isolate
前面講Flutter執行緒模型時,已經提到了isolate。它在底層其實就是個執行緒,但是dart vm限制了isolate的能力,使得isolate之間不能直接共享記憶體,只能通過Port機制收發訊息。
看一下程式碼
void main() async{
runApp(MyApp());
//asyncFibonacci函式裡會建立一個isolate,並返回執行結果
print(await asyncFibonacci(20));
}
//這裡以計算斐波那契數列為例,返回的值是Future,因為是非同步的
Future<dynamic> asyncFibonacci(int n) async{
final response = new ReceivePort();
await Isolate.spawn(isolateTask,response.sendPort);
final sendPort = await response.first as SendPort;
final answer = new ReceivePort();
sendPort.send([n,answer.sendPort]);
return answer.first;
}
//建立isolate必須要的引數
void isolateTask(SendPort initialReplyTo){
final port = new ReceivePort();
//繫結
initialReplyTo.send(port.sendPort);
//監聽
port.listen((message){
//獲取資料並解析
final data = message[0] as int;
final send = message[1] as SendPort;
//返回結果
send.send(syncFibonacci(data));
});
}
int syncFibonacci(int n){
return n < 2 ? n : syncFibonacci(n-2) + syncFibonacci(n-1);
}
複製程式碼
Port分為ReceivePort和SendPort,這兩者是成對出現的,在新建一個isolate的時候,可以傳入一個sendPort用於isolate向主執行緒發訊息,如果主執行緒想往子執行緒發訊息呢...就只能讓子執行緒new出一對port把sendport發過來才能用...
語法上是很囉嗦了,所幸Flutter給我們封裝了便捷的compute函式,可以參考深入瞭解Flutter的isolate(4) --- 使用Compute寫isolates,由於只是上層封裝,這裡就不具體展開了。
到這裡我們基本上明白了,isolate就是個削弱版的執行緒,用起來麻煩一點,另外就是由於不共享記憶體,port傳送資料時是copy的,如果有大塊記憶體真的要copy多份,可能會有比較大的記憶體問題。
但是,官方明確說明,dart是個單執行緒語言。這很容易讓人困惑,因為從體系結構上,isolate其實是核心級執行緒的封裝,在系統核心層面就是多執行緒的。
我覺得這句話可能從理論模式上理解會比較好。
併發程式設計長期以來有兩種正規化,一種是基於共享記憶體的,主要是多執行緒程式設計;一種是基於訊息的,如Actor、CSP模型。從這個角度看,isolate其實是訊息驅動的併發程式設計,算是CSP模型的簡化,跟多執行緒程式設計是完全不同的併發程式設計正規化。
因此說dart是個單執行緒語言也是說得通的。
宣告式UI
宣告式UI與響應式UI是對應的概念,考慮一下iOS/android的UI實現。
iOS是很純粹的命令式,new view,addsubview,new view,addsubview,這樣搞。
安卓呢,算是半命令式吧,xml宣告瞭UI,這是宣告式的部分;但程式執行時如果要修改某個view,仍是取到這個view,再去命令式地修改。
下面來看看flutter的框架
flutter的UI框架吸取了react的理念,即 UI是關於狀態的函式。
具體看一下demo
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
leading: IconButton(icon:Icon(Icons.arrow_back),
onPressed:() => SystemNavigator.pop(),
)
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
複製程式碼
這個是官方的helloworld demo。每個元件,會有個build函式,這裡會返回一個能夠完整描述UI的物件結構。每當資料改變時,就重新呼叫build函式,返回新的結構。如何高效渲染,就是框架去做的事情了。
通過這種方式,不管是UI的初始佈局結構,還是後面的修改,都是build函式返回的物件結構去宣告的,完整的宣告式UI由此而來。
UI開發的最佳實踐是怎麼樣的,一直以來都充滿爭議。但近幾年,React -> Flutter -> SwiftUI,都使用了宣告式的UI程式設計正規化,可以看到頭部公司基本上達成了共識,目前階段,這就是最佳實踐。