Flutter開發之非同步程式設計

xiangzhihong發表於2020-01-10

說到網路與通訊,就不得不提到非同步程式設計。所謂非同步程式設計,就是一種非阻塞的、事件驅動的程式設計機制,它可以充分利用系統資源來並行執行多個任務,因此提高了系統的執行效率。

事件迴圈機制

事件迴圈是Dart中處理事件的一種機制,與Android中的Handler訊息傳遞機制和前端的eventloop事件迴圈機制有點類似。在Flutter開發中,Flutter就是通過事件迴圈來驅動程式執行的。 眾所周知,Dart是一種單執行緒模型執行語言,這意味著Dart在同一時刻只能執行一個操作,其他操作需要在該操作執行完成之後才能執行,而多個操作的執行需要通過Dart的事件驅動模型,其執行流程如下圖所示。

在這裡插入圖片描述
入口main()函式執行完成之後,訊息迴圈機制便啟動了。Dart程式在啟動時會建立兩個佇列,一個是微任務佇列,另一個是事件佇列,並且微任務佇列的執行優先順序高於事件佇列。 首先,事件迴圈模型會按照先進先出的順序逐個執行微任務佇列中的任務,當所有微任務佇列執行完後便開始執行事件佇列中的任務,事件任務執行完畢後再去執行微任務,如此迴圈往復,直到應用退出。 在Dart中,所有的外部事件任務都在事件佇列中,如IO、計時器、點選、以及繪製事件等,而微任務則通常來源於Dart內部,並且微任務非常少。之所以如此,是因為微任務佇列優先順序高,如果微任務太多,那麼執行時間總和就越久,事件佇列任務的延遲也就越久。而對於GUI應用來說,最直觀的表現就是比較卡,所以Dart的事件迴圈模型必須保證微任務佇列不能太耗時。 由於Dart是一種單執行緒模型語言,所以當某個任務發生異常且沒有被捕獲時,程式並不會退出,而是直接阻塞當前任務後續程式碼的執行,但是並不會阻塞其他任務的執行,也就是說一個任務的異常是不會影響其它任務的執行。 可以看出,將任務加入到微任務中可以被儘快執行,但也需要注意,當事件迴圈在處理微任務佇列時,事件佇列會被卡住,此時應用程式無法處理滑鼠單擊、I/O訊息等事件。同時,當事件循壞出現異常時,也可以使用Dart提供的try/catch/finally來捕獲異常,並跳過異常執行其他事件。

Isolate

在Flutter開發中,經常會遇到耗時操作的場景,由於Dart是基於單執行緒模型的語言,所以耗時操作往往會堵塞其他程式碼的執行。為了解決這一問題,Dart提供了併發機制,即Isolate。 所謂Isolate,其實是Dart中的一個執行緒,不過與Java中的執行緒實現方式有所不同,Isolate是通過Flutter的Engine層建立出來的,Dart程式碼預設執行在主的Isolate上。當Dart程式碼處於執行狀態時,同一個Isolate中的其他程式碼是無法執行的。Flutter可以擁有多個Isolates,但多個Isolates之間不能共享記憶體,不同Isolate之間可以通過訊息機制來進行通訊。 同時,每個Isolate都擁有屬於自己的事件迴圈及訊息佇列,這意味著在一個Isolate中執行的程式碼與另外一個Isolate中的程式碼不存在任何關聯。也正是因為這一特性,才讓Dart具有了並行處理的能力。 預設情況下,Isolate是通過Flutter的Engine層建立出來的,Dart程式碼預設執行在主Isolate上,必要時還可以使用系統提供的API來建立新的Isolate,以便更好的利用系統資源,如主執行緒過載時。 在Dart中,建立Isolate主要有spawnUri和spawn兩種方式。與Isolate相關的程式碼都在isolate.dart檔案中,spawnUri的建構函式如下所示。

external static Future<Isolate> spawnUri(
      Uri uri,
      List<String> args,
      var message,
      {bool paused: false,
      SendPort onExit,
      SendPort onError,
      bool errorsAreFatal,
      bool checked,
      Map<String, String> environment,
      @Deprecated('The packages/ dir is not supported in Dart 2')
          Uri packageRoot,
      Uri packageConfig,
      bool automaticPackageResolution: false,
      @Since("2.3")
          String debugName});

複製程式碼

使用spawnUri方式建立Isolate時有三個必傳引數,分別是Uri、args和messag。其中,Uri用於指定一個新Isolate程式碼檔案的路徑,args用於表示引數列表,messag表示需要傳送的動態訊息。 需要注意的是,用於執行新Isolate的程式碼檔案必須包含一個main函式,它是新建立的Isolate的入口方法,並且main函式中的args引數與spawnUri中的args引數對應。如果不需要向新的Isolate中傳遞引數,可以向該引數傳遞一個空列表。首先,使用IntelliJ IDEA新建一個Dart工程,然後在主Isolate中新增如下程式碼。

import 'dart:isolate';

void main(List<String> arguments) {
  print("main isolate start");
  createIsolate();
  print("main isolate stop");
}

createIsolate() async{
  ReceivePort rp = new ReceivePort();
  SendPort port = rp.sendPort;
  Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_isolate.dart"), ["hello Isolate", "this is args"], port);
  SendPort sendPort;
  rp.listen((message){
    print("main isolate message: $message");
    if (message[0] == 0){
      sendPort = message[1];
    }else{
      sendPort?.send([1,"這條資訊是main Isolate傳送的"]);
    }
  });
}

複製程式碼

然後,在主Isolate檔案的同級目錄下新建一個other_isolate.dart檔案,程式碼如下。

import 'dart:isolate';
import  'dart:io';

void main(args, SendPort sendPort) {
  print("child isolate start");
  print("child isolate args: $args");
  ReceivePort receivePort = new ReceivePort();
  SendPort port = receivePort.sendPort;
  receivePort.listen((message){
    print("child_isolate message: $message");
  });

  sendPort.send([0, port]);
  sleep(Duration(seconds:5));
  sendPort.send([1, "child isolate 任務完成"]);
  print("child isolate stop");
}

複製程式碼

執行主Isolate檔案程式碼,最終的輸出結果如下。

main isolate start
main isolate stop
child isolate start
child isolate args: [hello Isolate, this is args]
main isolate message: [0, SendPort]
child isolate stop
main isolate message: [1, child isolate 任務完成]
child_isolate message: [1, 這條資訊是main Isolate傳送的]
複製程式碼

在Dart中,多個Isolate之間的通訊是通過ReceivePort來完成的。而ReceivePort可以認為是訊息管道,當訊息的傳遞方向時固定的,通過這個管道就能把訊息傳送給接收端。 除了使用spawnUri外,更常用的方式是使用spawn來建立Isolate,spawn的建構函式如下。

external static Future<Isolate> spawn<T>(
      void entryPoint(T message), T message,
      {bool paused: false,
      bool errorsAreFatal,
      SendPort onExit,
      SendPort onError});

複製程式碼

使用spawn方式建立Isolate時需要傳遞兩個引數,即函式entryPoint和引數message。entryPoint表示新建立的Isolate的耗時函式,message表示是動態訊息,該引數通常用於傳送主Isolate的SendPort物件。 通常,使用spawn方式建立Isolate時,我們希望將新建立的Isolate程式碼和主Isolate程式碼寫在同一個檔案,且不希望出現兩個main函式,並且將耗時函式執行在新的Isolate,這樣做的目的是有利於程式碼的組織與複用。

import 'dart:isolate';

Future<void> main(List<String> arguments) async {
  print(await asyncFibonacci(20));     //計算20的階乘
}

Future<dynamic> asyncFibonacci(int n) async{
  final response = new ReceivePort();
  await Isolate.spawn(isolate,response.sendPort);
  final sendPort = await response.first as SendPort;
  final answer = new ReceivePort();
  sendPort.send([n,answer.sendPort]);
  return answer.first;
}

void isolate(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);
}

複製程式碼

在上面的程式碼中,耗時的操作放在使用spawn方法建立的Isolate中。執行上面的程式,最終的輸出結果為6765,即20的階乘。

Flutter執行緒管理與Isolate

預設情況下,Flutter Engine層會建立一個Isolate,並且Dart程式碼預設就執行在這個主Isolate上。必要時可以使用spawnUri和spawn兩種方式來建立新的Isolate,在Flutter中,新建立的Isolate由Flutter進行統一的管理。 事實上,Flutter Engine自己不建立和管理執行緒,Flutter Engine執行緒的建立和管理是Embeder負責的,Embeder指的是將引擎移植到平臺的中間層程式碼,Flutter Engine層的架構示意圖如下圖所示。

在這裡插入圖片描述

在Flutter的架構中,Embeder提供四個Task Runner,分別是Platform Task Runner、UI Task Runner Thread、GPU Task Runner和IO Task Runner,每個Task Runner負責不同的任務,Flutter Engine不在乎Task Runner執行在哪個執行緒,但是它需要執行緒在整個生命週期裡面保持穩定。

Platform Task Runner

Platform Task Runner是Flutter Engine的主Task Runner,類似於Android或者iOS的Main Thread。不過它們之間還是有區別的,一般來說,一個Flutter應用啟動的時候會建立一個Engine例項,Engine建立的時候會建立一個執行緒供Platform Runner使用。 同時,跟Flutter Engine的所有互動都必須在Platform Thread中進行,如果試圖在其它執行緒中呼叫Flutter Engine可能會出現無法預期的異常,這跟iOS和Android中對於UI的操作都必須發生在主執行緒的道理類似。需要注意的是,Flutter Engine中有很多模組都是非執行緒安全的,因此對於Flutter Engine的介面呼叫都需保證在Platform Thread進行。 雖然阻塞Platform Thread不會直接導致Flutter應用的卡頓,但是也不建議在這個主Runner執行繁重的操作,因為長時間卡住Platform Thread有可能會被系統的Watchdog程式強殺。

UI Task Runner

UI Task Runner用於執行Root Isolate程式碼,它執行線上程對應平臺的執行緒上,屬於子執行緒。同時,Root isolate在引擎啟動時會繫結了不少Flutter需要的函式方法,以便進行渲染操作。 對於每一幀,引擎通過Root Isolate通知Flutter Engine有幀需要渲染,平臺收到Flutter Engine通知後會建立物件和元件並生成一個Layer Tree,然後將生成的Layer Tree提交給Flutter Engine。此時,只生成了需要繪製的內容,並沒有執行螢幕渲染,而Root Isolate就是負責將建立的Layer Tree繪製到螢幕上,因此如果執行緒過載會導致卡頓掉幀。 除了用於處理渲染之外,Root Isolate還需要處理來自Native Plugins的訊息響應、Timers、MicroTasks和非同步IO。如果確實有無法避免的繁重計算,建議將這些耗時的操作放到獨立的Isolate去執行,從而避免應用UI卡頓問題。

GPU Task Runner

GPU Task Runner用於執行裝置GPU指令,UI Task Runner建立的Layer Tree是跨平臺的。也就是說,Layer Tree提供了繪製所需要的資訊,但是由由誰來完成繪製它是不關心的。 GPU Task Runner負責將Layer Tree提供的資訊轉化為平臺可執行的GPU指令,GPU Task Runner同時也負責管理每一幀繪製所需要的GPU資源,包括平臺Framebuffer的建立,Surface生命週期管理,以及Texture和Buffers的繪製時機等。 一般來說,UI Runner和GPU Runner執行在不同的執行緒。GPU Runner會根據目前幀執行的進度去向UI Runner請求下一幀的資料,在任務繁重的時候還可能會出現UI Runner的延遲任務。不過這種排程機制的好處在於,確保GPU Runner不至於過載,同時也避免了UI Runner不必要的資源消耗。 GPU Runner可以導致UI Runner的幀排程的延遲,GPU Runner的過載會導致Flutter應用的卡頓,因此在實際使用過程中,建議為每一個Engine例項都新建一個專用的GPU Runner執行緒。

IO Task Runner

IO Task Runner也執行在平臺對應的子執行緒中,主要作用是做一些預先處理的讀取操作,為GPU Runner的渲染操作做準備。我們可以認為IO Task Runner是GPU Task Runner的助手,它可以減少GPU Task Runner的額外工作。例如,在Texture的準備過程中,IO Runner首先會讀取壓縮的圖片二進位制資料,並將其解壓轉換成GPU能夠處理的格式,然後再將資料傳遞給GPU進行渲染。 雖然IO Task Runner並不會直接導致Flutter應用的卡頓,但是可能會導致圖片和其它一些資源載入的延遲,並間接影響應用效能,所以建議將IO Runner放到一個專用的執行緒中。 Dart的Isolate是Dart虛擬機器建立和管理的,Flutter Engine無法直接訪問。Root Isolate通過Dart的C++呼叫能力把UI渲染相關的任務提交到UI Runner執行, 這樣就可以跟Flutter Engine模組進行互動,Flutter UI的任務也被提交到UI Runner,並可以給Isolate傳送一些事件通知,UI Runner同時也可以處理來自應用的Native Plugin任務。

總的來說,Dart Isolate跟Flutter Runner是相互獨立的,它們通過任務排程機制相互協作。

相關文章