Android開發者談:Flutter開發之異步編程

像程序那樣思考 發佈 2020-01-10T09:12:58+00:00

RootIsolate通過Dart的C++調用能力把UI渲染相關的任務提交到UI Runner執行, 這樣就可以跟Flutter Engine模塊進行交互,Flutter UI的任務也被提交到UI Runner,並可以給Isolate發送一些事件通知,UI Runner同時也可以

作者:xiangzhihong

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

事件循環機制

事件循環是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是相互獨立的,它們通過任務調度機制相互協作。


在這裡我也分享一份私貨,自己收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記,還有高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習

如果你有需要的話,可以點讚+評論+轉發關注我,然後私信我【進階】我發給你

關鍵字: