flutter入門:執行緒,非同步,宣告式UI

胖胖二師兄 發表於 2019-11-29

關於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的框架

Declarative UI

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程式設計正規化,可以看到頭部公司基本上達成了共識,目前階段,這就是最佳實踐。