在上一篇文章中,我帶你一起學習了如何在 Flutter 中實現動畫。對于組件動畫,Flutter 將動畫的狀態與渲染進行了分離,因此我們需要使用動畫曲線生成器 Animation、動畫狀態控制器 AnimationController 與動畫進度監聽器一起配合完成動畫更新;而對于跨頁面動畫,Flutter 提供了 Hero 組件,可以實現共享元素變換的頁面切換效果。
在之前的章節里,我們介紹了很多 Flutter 框架出色的渲染和交互能力。支撐起這些復雜的能力背后,實際上是基于單線程模型的 Dart。那么,與原生 Android 和 iOS 的多線程機制相比,單線程的 Dart 如何從語言設計層面和代碼運行機制上保證 Flutter UI 的流暢性呢?
因此今天,我會通過幾個小例子,循序漸進地向你介紹 Dart 語言的 Event Loop 處理機制、異步處理和并發編程的原理和使用方法,從語言設計和實踐層面理解 Dart 單線程模型下的代碼運行本質,從而懂得后續如何在工作中使用 Future 與 Isolate,優化我們的項目。
## Event Loop 機制
首先,我們需要建立這樣一個概念,那就是**Dart 是單線程的**。那單線程意味著什么呢?這意味著 Dart 代碼是有序的,按照在 main 函數出現的次序一個接一個地執行,不會被其他代碼中斷。另外,作為支持 Flutter 這個 UI 框架的關鍵技術,Dart 當然也支持異步。需要注意的是,**單線程和異步并不沖突。**
那為什么單線程也可以異步?
這里有一個大前提,那就是我們的 App 絕大多數時間都在等待。比如,等用戶點擊、等網絡請求返回、等文件 IO 結果,等等。而這些等待行為并不是阻塞的。比如說,網絡請求,Socket 本身提供了 select 模型可以異步查詢;而文件 IO,操作系統也提供了基于事件的回調機制。
所以,基于這些特點,單線程模型可以在等待的過程中做別的事情,等真正需要響應結果了,再去做對應的處理。因為等待過程并不是阻塞的,所以給我們的感覺就像是同時在做多件事情一樣。但其實始終只有一個線程在處理你的事情。
等待這個行為是通過 Event Loop 驅動的。事件隊列 Event Queue 會把其他平行世界(比如 Socket)完成的,需要主線程響應的事件放入其中。像其他語言一樣,Dart 也有一個巨大的事件循環,在不斷的輪詢事件隊列,取出事件(比如,鍵盤事件、I\\O 事件、網絡事件等),在主線程同步執行其回調函數,如下圖所示:
:-: 
圖 1 簡化版 Event Loop
## 異步任務
事實上,圖 1 的 Event Loop 示意圖只是一個簡化版。在 Dart 中,實際上有兩個隊列,一個事件隊列(Event Queue),另一個則是微任務隊列(Microtask Queue)。在每一次事件循環中,Dart 總是先去第一個微任務隊列中查詢是否有可執行的任務,如果沒有,才會處理后續的事件隊列的流程。
所以,Event Loop 完整版的流程圖,應該如下所示:
:-: 
圖 2 Microtask Queue 與 Event Queue
接下來,我們分別看一下這兩個隊列的特點和使用場景吧。
首先,我們看看微任務隊列。微任務顧名思義,表示一個短時間內就會完成的異步任務。從上面的流程圖可以看到,微任務隊列在事件循環中的優先級是最高的,只要隊列中還有任務,就可以一直霸占著事件循環。
微任務是由 scheduleMicroTask 建立的。如下所示,這段代碼會在下一個事件循環中輸出一段字符串:
~~~
scheduleMicrotask(() => print('This is a microtask'));
~~~
不過,一般的異步任務通常也很少必須要在事件隊列前完成,所以也不需要太高的優先級,因此我們通常很少會直接用到微任務隊列,就連 Flutter 內部,也只有 7 處用到了而已(比如,手勢識別、文本輸入、滾動視圖、保存頁面效果等需要高優執行任務的場景)。
異步任務我們用的最多的還是優先級更低的 Event Queue。比如,I/O、繪制、定時器這些異步事件,都是通過事件隊列驅動主線程執行的。
**Dart 為 Event Queue 的任務建立提供了一層封裝,叫作 Future**。從名字上也很容易理解,它表示一個在未來時間才會完成的任務。
把一個函數體放入 Future,就完成了從同步任務到異步任務的包裝。Future 還提供了鏈式調用的能力,可以在異步任務執行完畢后依次執行鏈路上的其他函數體。
接下來,我們看一個具體的代碼示例:分別聲明兩個異步任務,在下一個事件循環中輸出一段字符串。其中第二個任務執行完畢之后,還會繼續輸出另外兩段字符串:
~~~
Future(() => print('Running in Future 1'));// 下一個事件循環輸出字符串
Future(() => print(‘Running in Future 2'))
.then((_) => print('and then 1'))
.then((_) => print('and then 2’));// 上一個事件循環結束后,連續輸出三段字符串
~~~
當然,這兩個 Future 異步任務的執行優先級比微任務的優先級要低。
正常情況下,一個 Future 異步任務的執行是相對簡單的:在我們聲明一個 Future 時,Dart 會將異步任務的函數執行體放入事件隊列,然后立即返回,后續的代碼繼續同步執行。而當同步執行的代碼執行完畢后,事件隊列會按照加入事件隊列的順序(即聲明順序),依次取出事件,最后同步執行 Future 的函數體及后續的 then。
這意味著,**then 與 Future 函數體共用一個事件循環**。而如果 Future 有多個 then,它們也會按照鏈式調用的先后順序同步執行,同樣也會共用一個事件循環。
如果 Future 執行體已經執行完畢了,但你又拿著這個 Future 的引用,往里面加了一個 then 方法體,這時 Dart 會如何處理呢?面對這種情況,Dart 會將后續加入的 then 方法體放入微任務隊列,盡快執行。
下面的代碼演示了 Future 的執行規則,即,先加入事件隊列,或者先聲明的任務先執行;then 在 Future 結束后立即執行。
* 在第一個例子中,由于 f1 比 f2 先聲明,因此會被先加入事件隊列,所以 f1 比 f2 先執行;
* 在第二個例子中,由于 Future 函數體與 then 共用一個事件循環,因此 f3 執行后會立刻同步執行 then 3;
* 最后一個例子中,Future 函數體是 null,這意味著它不需要也沒有事件循環,因此后續的 then 也無法與它共享。在這種場景下,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'));
~~~
說了這么多規則,可能大家并沒有完全記住。那我們通過一個綜合案例,來把之前介紹的各個執行規則都串起來,再集中學習一下。
在下面的例子中,我們依次聲明了若干個異步任務 Future,以及微任務。在其中的一些 Future 內部,我們又內嵌了 Future 與 microtask 的聲明:
~~~
Future(() => print('f1'));// 聲明一個匿名 Future
Future fx = Future(() => null);// 聲明 Future fx,其執行體為 null
// 聲明一個匿名 Future,并注冊了兩個 then。在第一個 then 回調里啟動了一個微任務
Future(() => print('f2')).then((_) {
print('f3');
scheduleMicrotask(() => print('f4'));
}).then((_) => print('f5'));
// 聲明了一個匿名 Future,并注冊了兩個 then。第一個 then 是一個 Future
Future(() => print('f6'))
.then((_) => Future(() => print('f7')))
.then((_) => print('f8'));
// 聲明了一個匿名 Future
Future(() => print('f9'));
// 往執行體為 null 的 fx 注冊了了一個 then
fx.then((_) => print('f10'));
// 啟動一個微任務
scheduleMicrotask(() => print('f11'));
print('f12');
~~~
運行一下,上述各個異步任務會依次打印其內部執行結果:
~~~
f12
f11
f1
f10
f2
f3
f5
f4
f6
f9
f7
f8
~~~
看到這兒,你可能已經懵了。別急,我們先來看一下這段代碼執行過程中,Event Queue 與 Microtask Queue 中的變化情況,依次分析一下它們的執行順序為什么會是這樣的:
:-: 
圖 3 Event Queue 與 Microtask Queue 變化示例
* 因為其他語句都是異步任務,所以先打印 f12。
* 剩下的異步任務中,微任務隊列優先級最高,因此隨后打印 f11;然后按照 Future 聲明的先后順序,打印 f1。
* 隨后到了 fx,由于 fx 的執行體是 null,相當于執行完畢了,Dart 將 fx 的 then 放入微任務隊列,由于微任務隊列的優先級最高,因此 fx 的 then 還是會最先執行,打印 f10。
* 然后到了 fx 下面的 f2,打印 f2,然后執行 then,打印 f3。f4 是一個微任務,要到下一個事件循環才執行,因此后續的 then 繼續同步執行,打印 f5。本次事件循環結束,下一個事件循環取出 f4 這個微任務,打印 f4。
* 然后到了 f2 下面的 f6,打印 f6,然后執行 then。這里需要注意的是,這個 then 是一個 Future 異步任務,因此這個 then,以及后續的 then 都被放入到事件隊列中了。
* f6 下面還有 f9,打印 f9。
* 最后一個事件循環,打印 f7,以及后續的 f8。
上面的代碼很是燒腦,萬幸我們平時開發 Flutter 時一般不會遇到這樣奇葩的寫法,所以你大可放心。你只需要記住一點:**then 會在 Future 函數體執行完畢后立刻執行,無論是共用同一個事件循環還是進入下一個微任務。**
在深入理解 Future 異步任務的執行規則之后,我們再來看看怎么封裝一個異步函數。
## 異步函數
對于一個異步函數來說,其返回時內部執行動作并未結束,因此需要返回一個 Future 對象,供調用者使用。調用者根據 Future 對象,來決定:是在這個 Future 對象上注冊一個 then,等 Future 的執行體結束了以后再進行異步處理;還是一直同步等待 Future 執行體結束。
對于異步函數返回的 Future 對象,如果調用者決定同步等待,則需要在調用處使用 await 關鍵字,并且在調用處的函數體使用 async 關鍵字。
在下面的例子中,異步方法延遲 3 秒返回了一個 Hello 2019,在調用處我們使用 await 進行持續等待,等它返回了再打印:
~~~
// 聲明了一個延遲 3 秒返回 Hello 的 Future,并注冊了一個 then 返回拼接后的 Hello 2019
Future<String> fetchContent() =>
Future<String>.delayed(Duration(seconds:3), () => "Hello")
.then((x) => "$x 2019");
main() async{
print(await fetchContent());// 等待 Hello 2019 的返回
}
~~~
也許你已經注意到了,我們在使用 await 進行等待的時候,在等待語句的調用上下文函數 main 加上了 async 關鍵字。為什么要加這個關鍵字呢?
因為**Dart 中的 await 并不是阻塞等待,而是異步等待**。Dart 會將調用體的函數也視作異步函數,將等待語句的上下文放入 Event Queue 中,一旦有了結果,Event Loop 就會把它從 Event Queue 中取出,等待代碼繼續執行。
接下來,為了幫助你加深印象,我準備了兩個具體的案例。
我們先來看下這段代碼。第二行的 then 執行體 f2 是一個 Future,為了等它完成再進行下一步操作,我們使用了 await,期望打印結果為 f1、f2、f3、f4:
~~~
Future(() => print('f1'))
.then((_) async => await Future(() => print('f2')))
.then((_) => print('f3'));
Future(() => print('f4'));
~~~
實際上,當你運行這段代碼時就會發現,打印出來的結果其實是 f1、f4、f2、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 并不會被它阻塞。
接下來,我們再看另一個例子:在主函數調用一個異步函數去打印一段話,而在這個異步函數中,我們使用 await 與 async 同步等待了另一個異步函數返回字符串:
~~~
// 聲明了一個延遲 2 秒返回 Hello 的 Future,并注冊了一個 then 返回拼接后的 Hello 2019
Future<String> fetchContent() =>
Future<String>.delayed(Duration(seconds:2), () => "Hello")
.then((x) => "$x 2019");
// 異步函數會同步等待 Hello 2019 的返回,并打印
func() async => print(await fetchContent());
main() {
print("func before");
func();
print("func after");
}
~~~
運行這段代碼,我們發現最終輸出的順序其實是“func before”“func after”“Hello 2019”。func 函數中的等待語句似乎沒起作用。這是為什么呢?
同樣,我來給你分析一下這段代碼的執行順序:
* 首先,第一句代碼是同步的,因此先打印“func before”。
* 然后,進入 func 函數,func 函數調用了異步函數 fetchContent,并使用 await 進行等待,因此我們把 fetchContent、await 語句的上下文函數 func 先后放入事件隊列。
* await 的上下文函數并不包含調用棧,因此 func 后續代碼繼續執行,打印“func after”。
* 2 秒后,fetchContent 異步任務返回“Hello 2019”,于是 func 的 await 也被取出,打印“Hello 2019”。
通過上述分析,你發現了什么現象?那就是 await 與 async 只對調用上下文的函數有效,并不向上傳遞。因此對于這個案例而言,func 是在異步等待。如果我們想在 main 函數中也同步等待,需要在調用異步函數時也加上 await,在 main 函數也加上 async。
經過上面兩個例子的分析,你應該已經明白 await 與 async 是如何配合,完成等待工作的了吧。
介紹完了異步,我們再來看在 Dart 中,如何通過多線程實現并發。
## Isolate
盡管 Dart 是基于單線程模型的,但為了進一步利用多核 CPU,將 CPU 密集型運算進行隔離,Dart 也提供了多線程機制,即 Isolate。在 Isolate 中,資源隔離做得非常好,每個 Isolate 都有自己的 Event Loop 與 Queue,**Isolate 之間不共享任何資源,只能依靠消息機制通信,因此也就沒有資源搶占問題**。
和其他語言一樣,Isolate 的創建非常簡單,我們只要給定一個函數入口,創建時再傳入一個參數,就可以啟動 Isolate 了。如下所示,我們聲明了一個 Isolate 的入口函數,然后在 main 函數中啟動它,并傳入了一個字符串參數:
~~~
doSth(msg) => print(msg);
main() {
Isolate.spawn(doSth, "Hi");
...
}
~~~
但更多情況下,我們的需求并不會這么簡單,不僅希望能并發,還希望 Isolate 在并發執行的時候告知主 Isolate 當前的執行結果。
對于執行結果的告知,Isolate 通過發送管道(SendPort)實現消息通信機制。我們可以在啟動并發 Isolate 時將主 Isolate 的發送管道作為參數傳給它,這樣并發 Isolate 就可以在任務執行完畢后利用這個發送管道給我們發消息了。
下面我們通過一個例子來說明:在主 Isolate 里,我們創建了一個并發 Isolate,在函數入口傳入了主 Isolate 的發送管道,然后等待并發 Isolate 的回傳消息。在并發 Isolate 中,我們用這個管道給主 Isolate 發了一個 Hello 字符串:
~~~
Isolate isolate;
start() async {
ReceivePort receivePort= ReceivePort();// 創建管道
// 創建并發 Isolate,并傳入發送管道
isolate = await Isolate.spawn(getMsg, receivePort.sendPort);
// 監聽管道消息
receivePort.listen((data) {
print('Data:$data');
receivePort.close();// 關閉管道
isolate?.kill(priority: Isolate.immediate);// 殺死并發 Isolate
isolate = null;
});
}
// 并發 Isolate 往管道發送一個字符串
getMsg(sendPort) => sendPort.send("Hello");
~~~
這里需要注意的是,在 Isolate 中,發送管道是單向的:我們啟動了一個 Isolate 執行某項任務,Isolate 執行完畢后,發送消息告知我們。如果 Isolate 執行任務時,需要依賴主 Isolate 給它發送參數,執行完畢后再發送執行結果給主 Isolate,這樣**雙向通信的場景我們如何實現呢**?答案也很簡單,讓并發 Isolate 也回傳一個發送管道即可。
接下來,我們以一個**并發計算階乘**的例子來說明如何實現雙向通信。
在下面的例子中,我們創建了一個異步函數計算階乘。在這個異步函數內,創建了一個并發 Isolate,傳入主 Isolate 的發送管道;并發 Isolate 也回傳一個發送管道;主 Isolate 收到回傳管道后,發送參數 N 給并發 Isolate,然后立即返回一個 Future;并發 Isolate 用參數 N,調用同步計算階乘的函數,返回執行結果;最后,主 Isolate 打印了返回結果:
~~~
// 并發計算階乘
Future<dynamic> asyncFactoriali(n) async{
final response = ReceivePort();// 創建管道
// 創建并發 Isolate,并傳入管道
await Isolate.spawn(_isolate,response.sendPort);
// 等待 Isolate 回傳管道
final sendPort = await response.first as SendPort;
// 創建了另一個管道 answer
final answer = ReceivePort();
// 往 Isolate 回傳的管道中發送參數,同時傳入 answer 管道
sendPort.send([n,answer.sendPort]);
return answer.first;// 等待 Isolate 通過 answer 管道回傳執行結果
}
//Isolate 函數體,參數是主 Isolate 傳入的管道
_isolate(initialReplyTo) async {
final port = ReceivePort();// 創建管道
initialReplyTo.send(port.sendPort);// 往主 Isolate 回傳管道
final message = await port.first as List;// 等待主 Isolate 發送消息 (參數和回傳結果的管道)
final data = message[0] as int;// 參數
final send = message[1] as SendPort;// 回傳結果的管道
send.send(syncFactorial(data));// 調用同步計算階乘的函數回傳結果
}
// 同步計算階乘
int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
main() async => print(await asyncFactoriali(4));// 等待并發計算階乘結果
~~~
看完這段代碼你是什么感覺呢?我們只是為了并發計算一個階乘,這樣是不是太繁瑣了?
沒錯,確實太繁瑣了。在 Flutter 中,像這樣執行并發計算任務我們可以采用更簡單的方式。Flutter 提供了支持并發計算的 compute 函數,其內部對 Isolate 的創建和雙向通信進行了封裝抽象,屏蔽了很多底層細節,我們在調用時只需要傳入函數入口和函數參數,就能夠實現并發計算和消息通知。
我們試著用 compute 函數改造一下并發計算階乘的代碼:
~~~
// 同步計算階乘
int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
// 使用 compute 函數封裝 Isolate 的創建和結果的返回
main() async => print(await compute(syncFactorial, 4));
~~~
可以看到,用 compute 函數改造以后,整個代碼就變成了兩行,現在并發計算階乘的代碼看起來就清爽多了。
## 總結
好了,今天關于 Dart 的異步與并發機制、實現原理的分享就到這里了,我們來簡單回顧一下主要內容。
Dart 是單線程的,但通過事件循環可以實現異步。而 Future 是異步任務的封裝,借助于 await 與 async,我們可以通過事件循環實現非阻塞的同步等待;Isolate 是 Dart 中的多線程,可以實現并發,有自己的事件循環與 Queue,獨占資源。Isolate 之間可以通過消息機制進行單向通信,這些傳遞的消息通過對方的事件循環驅動對方進行異步處理。
在 UI 編程過程中,異步和多線程是兩個相伴相生的名詞,也是很容易混淆的概念。對于異步方法調用而言,代碼不需要等待結果的返回,而是通過其他手段(比如通知、回調、事件循環或多線程)在后續的某個時刻主動(或被動)地接收執行結果。
因此,從辯證關系上來看,異步與多線程并不是一個同等關系:異步是目的,多線程只是我們實現異步的一個手段之一。而在 Flutter 中,借助于 UI 框架提供的事件循環,我們可以不用阻塞的同時等待多個異步任務,因此并不需要開多線程。我們一定要記住這一點。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/23_dart_async)中,你可以下載下來,反復運行幾次,加深理解。
## 思考題
最后,我給你留下兩道思考題吧。
1. 在通過并發 Isolate 計算階乘的例子中,我在 asyncFactoriali 方法里先后發給了并發 Isolate 兩個 SendPort。你能否解釋下這么做的原因?可以只發一個 SendPort 嗎?
2. 請改造以下代碼,在不改變整體異步結構的情況下,實現輸出結果為 f1、f2、f3、f4。
~~~
Future(() => print('f1'))
.then((_) async => await Future(() => print('f2')))
.then((_) => print('f3'));
Future(() => print('f4'));
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略