Flutter中Dart異步模型

前言

咱們知道Flutter 框架有出色的渲染和交互能力。支撐起這些複雜的能力背後,其實是基於單線程模型的 Dart。那麼,與原生 Android 和 iOS 的多線程機制相比,單線程的 Dart 如何從語言設計層面和代碼運行機制上保證 Flutter UI 的流暢性呢?web

單線程模型

咱們從下面幾個方面闡述一下:編程

  1. Dart 語言單線程模型和 Event Loop 處理機制
  2. 異步處理和併發編程的原理和使用方法
  3. Dart 單線程模型下的代碼運行本質

1. Dart單線程模型

dart是單線程運行的。怎麼理解這句話呢, 從下面幾個方面能夠看到這個設計思想.bash

1.1 默認單一運行的線程

dart默認運行在Main函數存在線程,在dart中稱之爲isolate,這個線程咱們可稱之爲main isolate。單線程任務處理的,若是不開啓新的isolate,任務默認在主isolate中處理。一旦 Dart 函數執行,它將按照在 main 函數出現的次序一個接一個地持續執行,直到退出。換而言之,Dart 函數在執行期間,沒法被其餘 Dart 代碼打斷。網絡

1.2 獨享內存

Android和IOS能夠自由的開闢除了UI主線程以外的線程,這些線程和主線程能夠共享內存的變量,可是, Dart中的isolate沒法共享內存。Isolate 不能共享內存,他們就像是單獨的分離的 app,經過消息進行溝通。除了顯式指定代碼運行在別的 isolate 或者 worker 中,其餘代碼都運行在 app 的 main isolate 中。更多信息能夠訪問Use isolates or workers if necessary多線程

1.3 質疑

(1)假若有一個任務(讀寫文件或者網絡)耗時10秒,而且加入到了事件任務隊列中,執行單這個任務的時候不就把線程卡主嗎?併發

答:文件I/O和網絡調用並非在Dart層作的,而是由操做系統提供的異步線程,他倆把活兒幹完以後把結果剛到隊列中,Dart代碼只是執行一個簡單的讀動做。app

(2)單線程模型是指的事件隊列模型,和繪製界面的線程是一個嗎?框架

答:咱們所說的單線程指的是主Isolate。而GPU繪製指令有單獨的線程執行,跟主Isolate無關。事實上Flutter提供了4種task runner,有獨立的線程去運行專屬的任務:參見:深刻理解Flutter引擎線程模式異步

  1. Platform Task Runner:處理來自平臺(Android/iOS)的消息
  2. UI Task Runner:執行渲染邏輯、處理native plugin的消息、timer、microtask、異步I/O操做處理等
  3. GPU Task Runner:執行GPU指令
  4. IO Task Runner:執行I/O任務

2. Event Loop 機制

消息隊列模型

如圖所示,dart也存在事件隊列和事件循環。每一個isolate也包含一個事件循環,區別是他有兩個事件隊列,event loop事件循環,以及event queue和microtask queue事件隊列,event和microtask隊列有點相似iOS的source0和source1。async

  • event queue:負責處理I/O事件、繪製事件、手勢事件、接收其餘isolate消息等外部事件。
  • microtask queue:能夠本身向isolate內部添加事件,事件的優先級比event queue高。
事件隊列模型
  1. 先檢查MicroTask隊列是否爲空,非空則先執行MicroTask隊列中的MicroTask
  2. 一個MicroTask執行完後,檢查有沒有下一個MicroTask,直到MicroTask隊列爲空,纔去執行Event隊列
  3. Evnet 隊列取出一個事件處理完後,再次返回第一步,去檢查MicroTask隊列是否爲空

咱們能夠看出,將任務加入到MicroTask中能夠被儘快執行,但也須要注意,當事件循環在處理MicroTask隊列時,會阻塞event隊列的事件執行,這樣就會致使渲染、手勢響應等event事件響應延時。爲了保證渲染和手勢響應,應該儘可能將耗時操做放在event隊列中。

咱們一般不多會直接用到微任務隊列,就連 Flutter 內部,也只有 7 處用到了而已(好比,手勢識別、文本輸入、滾動視圖、保存頁面效果等須要高優執行任務的場景)。

簡單總結爲一二一模型:1個事件循環和2個隊列的單線程執行模型。

3. 異步任務調度

爲何單線程也能夠異步?這裏有一個大前提,那就是咱們的 App 絕大多數時間都在等待。好比,等用戶點擊、等網絡請求返回、等文件 IO 結果,等等。而這些等待行爲並非阻塞的。好比說,網絡請求,Socket 自己提供了 select 模型能夠異步查詢;而文件 IO,操做系統也提供了基於事件的回調機制。因此,基於這些特色,單線程模型能夠在等待的過程當中作別的事情,等真正須要響應結果了,再去作對應的處理。由於等待過程並非阻塞的,因此給咱們的感受就像是同時在作多件事情同樣。但其實始終只有一個線程在處理你的事情。

異步任務咱們用的最多的仍是優先級更低的 Event Queue。好比,I/O、繪製、定時器這些異步事件,都是經過事件隊列驅動主線程執行的。

3.1 用Future發起異步任務

Dart 爲 Event Queue 的任務創建提供了一層封裝,叫做 Future。Future 還提供了鏈式調用的能力,能夠在異步任務執行完畢後依次執行鏈路上的其餘函數體。

new Future((){
    //  doing something
});複製代碼

微任務是由 scheduleMicroTask 創建的。以下所示,這段代碼會在下一個事件循環中輸出一段字符串:

scheduleMicrotask(() => print('This is a microtask'));複製代碼

鏈式調用:

Future(() => print('Running in Future 1'));//下一個事件循環輸出字符串

Future(() => print(‘Running in Future 2')) .then((_) => print('and then 1')) .then((_) => print('and then 2’));//上一個事件循環結束後,連續輸出三段字符串複製代碼

Dart 會將異步任務的函數執行體放入事件隊列,而後當即返回,後續的代碼繼續同步執行。而當同步執行的代碼執行完畢後,事件隊列會按照加入事件隊列的順序(即聲明順序),依次取出事件,最後同步執行 Future 的函數體及後續的 then。這意味着,then 與 Future 函數體共用一個事件循環。而若是 Future 有多個 then,它們也會按照鏈式調用的前後順序同步執行,一樣也會共用一個事件循環。

若是 Future 執行體已經執行完畢了,但你又拿着這個 Future 的引用,往裏面加了一個 then 方法體,這時 Dart 會如何處理呢?面對這種狀況,Dart 會將後續加入的 then 方法體放入微任務隊列,儘快執行。

//f1比f2先執行
Future(() => print('f1'));
Future(() => print('f2'));

//f3執行後會馬上同步執行then 3
Future(() => print('f3')).then((_) => print('then 3'));

//then 4會加入微任務隊列,儘快執行
Future(() => null).then((_) => print('then 4'));
結果: f1 f2 f3 then 3 then 4複製代碼

4. 異步函數

Future 是異步任務的封裝,藉助於 await 與 async,咱們能夠經過事件循環實現非阻塞的同步等待。Dart 中的 await 並非阻塞等待,而是異步等待。Dart 會將調用體的函數也視做異步函數,將等待語句的上下文放入 Event Queue 中,一旦有告終果,Event Loop 就會把它從 Event Queue 中取出,等待代碼繼續執行。

async關鍵字做爲方法聲明的後綴時,具備以下意義

  • 被修飾的方法會將一個 Future 對象做爲返回值
  • 該方法會同步執行其中的方法的代碼直到第一個 await 關鍵字,而後它暫停該方法其餘部分的執行;
  • 一旦由 await 關鍵字引用的 Future 任務執行完成,await的下一行代碼將當即執行。
// 導入io庫,調用sleep函數
import 'dart:io';

// 模擬耗時操做,調用sleep函數睡眠2秒
doTask() async{
  await sleep(const Duration(seconds:2));
  return "Ok";
}

// 定義一個函數用於包裝
test() async {
  var r = await doTask();
  print(r);
}

void main(){
  print("main start");
  test();
  print("main end");
}
結果:
main start
main end
Ok複製代碼

咱們先來看下這段代碼。第二行的 then 執行體 f2 是一個 Future,爲了等它完成再進行下一步操做,咱們使用了 await,指望打印結果爲 f一、f二、f三、f4:

Future(()=>print('f1'))
.then((_)async=>awaitFuture(()=>print('f2')))
.then((_)=>print('f3'));
Future(()=>print('f4'));複製代碼

實際上,當你運行這段代碼時就會發現,打印出來的結果實際上是 f一、f四、f二、f3!

  • 分析一下這段代碼的執行順序:
  • 按照任務的聲明順序,f1 和 f4 被前後加入事件隊列。
  • f1 被取出並打印;
  • 而後到了 then。then 的執行體是個 future f2,因而放入 Event Queue。
  • 而後把 await 也放到 Event Queue 裏。這個時候要注意了,Event Queue 裏面還有一個 f4,咱們的 await 並不能阻塞 f4 的執行。所以,Event Loop 先取出 f4,打印 f4;
  • 而後才能取出並打印 f2,最後把等待的 await 取出,開始執行後面的 f3。

因爲 await 是採用事件隊列的機制實現等待行爲的,因此比它先在事件隊列中的 f4 並不會被它阻塞。

5. Isolate

Dart 也提供了多線程機制,即 Isolate(這個單詞的中文意思是隔離)。在 Isolate 中,資源隔離作得很是好,每一個 Isolate 都有本身的 Event Loop 與 Queue,Isolate 之間不共享任何資源,只能依靠消息機制通訊,所以也就沒有資源搶佔問題。以下所示,咱們聲明瞭一個 Isolate 的入口函數,而後在 main 函數中啓動它,並傳入了一個字符串參數:

doSth(msg) => print(msg);

main() {
  Isolate.spawn(doSth, "Hi");
  ...
}複製代碼

那麼如何利用消息機制進行通訊呢,下面引用了一篇文章的講解,圖畫的很好。

引用

整個消息通訊過程如上圖所示,兩個Isolate是經過兩對Port對象通訊,一對Port分別由用於接收消息的ReceivePort對象,和用於發送消息的SendPort對象構成。其中SendPort對象不用單首創建,它已經包含在ReceivePort對象之中。須要注意,一對Port對象只能單向發消息,這就如同一根自來水管,ReceivePortSendPort分別位於水管的兩頭,水流只能從SendPort這頭流向ReceivePort這頭。所以,兩個Isolate之間的消息通訊確定是須要兩根這樣的水管的,這就須要兩對Port對象。

6. 引用文章

(1)23 | 單線程模型怎麼保證UI運行流暢?

(2)Dart 異步編程詳解之一文全懂

(3)Dart asynchronous programming: Isolates and event loops

相關文章
相關標籤/搜索