不知道大家有沒有一個疑問:Dart是單執行緒執行,那它是如何實現非同步操作的呢?
本文將對Dart/Flutter提供的Isolate
,Event Loop
,Future
,async/await
等進行非同步操作相關的知識點進行分析。
Isolate
什麼是Isolate?
An isolate is what all Dart code runs in. It’s like a little space on the machine with its own, private chunk of memory and a single thread running an event loop.
- Isolate相當於
Dart
語言中的執行緒Thread
,是Dart/Flutter
的執行上下文環境(容器); - Isolate有自己獨立的記憶體地址和
Event Loop
,不存在共享記憶體所以不會出現死鎖,但是比Thread
更耗記憶體;
- Isolate間不能直接訪問,需憑藉
Port
進行通訊;
Main Isolate
當執行完main()
入口函式後,Flutter會建立一個Main Isolate。一般情況下任務都是在這個Main Isolate中執行的。
多執行緒
一般情況下在Main Isolate執行任務是可以接受的,但是把一些耗時操作放在Main Isolate中執行,會造成掉幀的現象,這會對使用者體驗造成嚴重影響。此時,選擇將耗時任務分發到其他的Isolate中就是一個很好的實現方式了。
所有的Dart Code都是在Isolate
中執行的,程式碼只能使用同一個Isolate
中的內容,不同的 Isolate
是記憶體隔離的,因此只能通過 Port 機制傳送訊息通訊,其原理是向不同的 Isolate 佇列中執行寫任務。
案例
我們做了個簡單的Demo,螢幕中間有一個心在不停的動畫(由小變大,再由大變小)。當我們點選右下角對的加號按鈕,會進行一個耗時的運算。如果耗時操作在Main Isolate執行,將會造成介面的丟幀,動畫將會出現卡頓的情況。
我們目前就是需要解決這個掉幀的問題。
1.compute
方法
Flutter封裝了一個compute
這個高階API函式可以讓我們方便的實現多執行緒的功能。
Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
}
複製程式碼
compute
接收兩個必傳引數:1,需要執行的方法;2,傳入的引數,這引數最多隻能是1個,所以多個引數需要封裝到Map
中;
- 最開始的程式碼
// 耗時操作的方法:`bigCompute`
Future<int> bigCompute(int initalNumber) async {
int total = initalNumber;
for (var i = 0; i < 1000000000; i++) {
total += i;
}
return total;
}
// 點選按鈕呼叫的方法:`calculator`
void calculator() async {
int result = await bigCompute(0);
print(result);
}
// FloatingActionButton的點選事件
FloatingActionButton(
onPressed: calculator,
tooltip: 'Increment',
child: Icon(Icons.add),
)
複製程式碼
- 修改程式碼
- 新建一個
calculatorByComputeFunction
方法,用compute
呼叫bigCompute
方法:
void calculatorByComputeFunction() async {
// 使用`compute`呼叫`bigCompute`方法,傳參0
int result = await compute(bigCompute, 0);
print(result);
}
複製程式碼
- 修改FloatingActionButton的點選事件方法為
calculatorByComputeFunction
FloatingActionButton(
onPressed: calculatorByComputeFunction,
tooltip: 'Increment',
child: Icon(Icons.add),
)
複製程式碼
我們點選試試?
[VERBOSE-2:ui_dart_state.cc(186)] Unhandled Exception: Invalid argument(s): Illegal argument in isolate message : (object is a closure - Function 'bigCompute':.)
- 解決Error:將
bigCompute
改為為static方法(改為全域性函式也是可行的)
static Future<int> bigCompute(int initalNumber) async {
int total = initalNumber;
for (var i = 0; i < 1000000000; i++) {
total += i;
}
return total;
}
複製程式碼
警告:還有一個需要注意的是所有的
Platform-Channel
的通訊必須在Main Isolate中執行,譬如在其他Isolate中呼叫rootBundle.loadString("assets/***")
就掉坑裡了。
2. 直接使用Isolate
上面我們用compute
方法,基本上沒有看到Isolate
的身影,因為Flutter幫我們做了很多工作,包括Isolate
建立,銷燬,方法的執行等等。一般情況下我們使用這個方法就夠了。
但是這個方法有個缺陷,我們只能執行一個任務,當我們有多個類似的耗時操作時候,如果使用這個compute
方法將會出現大量的建立和銷燬,是一個高消耗的過程,如果能複用Isolate
那就是最好的實現方式了。
多執行緒Isolate
間通訊的原理如下:
-
當前
Isolate
接收其他Isolate
訊息的實現邏輯:Isolate
之間是通過Port進行通訊的,ReceivePort
是接收器,它配套有一個SendPort
傳送器, 當前Isolate
可以把SendPort
傳送器送給其他Isolate
,其他Isolate
通過這個SendPort
傳送器就可以傳送訊息給當前Isolate
了。 -
當前
Isolate
給其他Isolate
發訊息的實現邏輯: 其他Isolate
通過當前Isolate
的SendPort
傳送器傳送一個SendPort2
傳送器2過來,其他的Isolate
則持有SendPort 2
傳送器2對應的接收器ReceivePort2
接收器2,當前Isolate
通過SendPort 2
傳送訊息就可以被其他Isolate
收到了。
是不是很繞!我再打個比喻:市面上有一套通訊工具套件,這套通訊工具套件包括一個接電話的工具和一個打電話的工具。A留有接電話的,把打電話的送給B,這樣B就可以隨時隨地給A打電話了(此時是單向通訊)。 如果B也有一套工具,把打電話的送給A,這樣A也能隨時隨地給B打電話了(此時是雙向通訊了)。
上程式碼:
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
// 1.1 新建的isolate
Isolate isolate;
// 1.2 Main Isolate的接收器
ReceivePort mainIsolaiteReceivePort;
// 1.3 Other Isolate的傳送器
SendPort otherIsolateSendPort;
// 新建(複用)Isolate
void spawnNewIsolate() async {
// 2.1 建一個接收Main Isolate的接收器
if (mainIsolaiteReceivePort == null) {
mainIsolaiteReceivePort = ReceivePort();
}
try {
if (isolate == null) {
// 2.2 新建的isolate, 把Main Isolate傳送器傳給新的isolate,calculatorByIsolate是需要執行的任務
isolate = await Isolate.spawn(
calculatorByIsolate, mainIsolaiteReceivePort.sendPort);
// 2.3 Main Isolate 通過接收器接收新建的isolate發來的訊息
mainIsolaiteReceivePort.listen((dynamic message) {
if (message is SendPort) {
// 2.4 如果新建的isolate發來的是一個傳送器,就通過這個傳送器給新建的isolate傳送值過去(此時雙向通訊建立成功)
otherIsolateSendPort = message;
otherIsolateSendPort.send(1);
print("雙向通訊建立成功,主isolate傳遞初始引數1");
} else {
// 2.5 如果新建的isolate發來了一個值,我們知道是耗時操作的計算結果。
print("新建的isolate計算得到的結果$message");
}
});
} else {
// 2.6 複用otherIsolateSendPort
if (otherIsolateSendPort != null) {
otherIsolateSendPort.send(1);
print("雙向通訊複用,主isolate傳遞初始引數1");
}
}
} catch (e) {}
}
// 這個是新的Isolate中執行的任務
static void calculatorByIsolate(SendPort sendPort) {
// 3.1 新的Isolate把傳送器發給Main Isolate
ReceivePort receivePort = new ReceivePort();
sendPort.send(receivePort.sendPort);
// 3.2 如過Main Isolate發過來了初始資料,就可以進行耗時計算了
receivePort.listen((val) {
print("從主isolate傳遞過來的初始引數是$val");
int total = val;
for (var i = 0; i < 1000000000; i++) {
total += i;
}
// 3.3 通過Main Isolate的傳送器發給Main Isolate計算結果
sendPort.send(total);
});
}
@override
void dispose() {
// 釋放資源
mainIsolaiteReceivePort.close();
isolate.kill();
super.dispose();
}
}
複製程式碼
程式碼註釋的很詳細了,就不再解釋了。是不是程式碼好多的感覺,其實如果理解流程了邏輯倒不復雜。
關於Isolate
的概念和使用我們就介紹到這裡,接下來我們來介紹Isolate
中的一個重要知識點Event Loop
.
Event Loop
Loop
這個概念絕大部分開發者都應該很熟悉了,iOS中有NSRunLoop
,Android中有Looper
, js中有Event Loop
,名字上類似,其實所做的事情也是類似的。
Event Loop的官方介紹如下圖:
- 靜態示意圖
執行完
main()
函式後將會建立一個Main Isolate
。
- 動態示意圖
- Event Loop會處理兩個佇列
MicroTask queue
和Event queue
中的任務; Event queue
主要處理外部的事件任務:I/O
,手勢事件
,定時器
,isolate間的通訊
等;MicroTask queue
主要處理內部的任務:譬如處理I/O
事件的中間過程中可能涉及的一些特殊處理等;- 兩個佇列都是先進先出的處理邏輯,優先處理
MicroTask queue
的任務,當MicroTask queue
佇列為空後再執行Event queue
中的任務; - 當兩個佇列都為空的時候就進行GC操作,或者僅僅是在等待下個任務的到來。
為了比較好的理解 Event Loop 的非同步邏輯,我們來打個比喻:就像我去長沙某網紅奶茶品牌店買杯“幽蘭拿鐵”(由於是現做的茶,比較耗時)的過程。
- 我來到前臺給服務員說我要買一杯你們店的“幽蘭拿鐵”,然後服務員遞給了我一個有編號的飛盤(獲取憑證);
- 奶茶店的備餐員工就將我的訂單放在訂單列表的最後面,他們按照順序準備訂單上的商品,準備好一個就讓顧客去領取(Event queue 先進先出進行處理),而我就走開了,該幹啥幹啥去了(非同步過程,不等待處理結果);
- 突然他們來了個超級VIP會員的訂單,備餐員工就把這個超級VIP訂單放在了其他訂單的最前面,優先安排了這個訂單的商品(MicroTask優先處理)---此場景為虛構;
- 當我的訂單完成後,飛盤開始震動(進行結果回撥),我又再次回到了前臺,如果前臺妹子遞給我一杯奶茶(獲得結果),如果前臺妹子說對不起先生,到您的訂單的時候沒水了,訂單沒法完成了給我退錢(獲得異常錯誤錯誤)。
我們常用的非同步操作Future
,async
,await
都是基於Event Loop,我們接下來就來介紹他們非同步操作背後的原理。
Future
我們接下來用程式碼總體說明一下Future
背後的邏輯:
final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {
setImage(resp);
}).catchError((err) {
print('Caught $err'); // Handle the error.
});
// 繼續其他任務
...
複製程式碼
http.get('https://my.image.url')
返回的是一個未完成狀態的Future
, 可以理解為一個控制程式碼,同時http.get('https://my.image.url')
被丟進了Event queue
中等待被執行,然後接著執行當前的其他任務;- 當
Event queue
執行完這個get
請求成功後會回撥then
方法,將結果返回,Future
為完成狀態 ,就可以進行接下來的操作了;- 當
Event queue
執行完這個get
請求失敗後會回撥catchError
方法,將錯誤返回,Future
為失敗狀態 ,就可以進行錯誤處理了。
我們接下來分別介紹下Future
的一些相關函式:
建構函式
Future(FutureOr<T> computation())
final future1 = Future(() {
return 1;
});
複製程式碼
computation
被放入了Event queue
佇列中
Future.value
final future2 = Future.value(2);
複製程式碼
值在
MicroTask queue
佇列中返回
Future.error(Object error, [StackTrace? stackTrace])
final future3 = Future.error(3);
複製程式碼
這個
error
表示出現了錯誤,其中的值不一定需要給一個Error
物件
Future.delay
final future4 = Future.delayed(Duration(seconds: 1), () {
return 4;
});
複製程式碼
延遲一定時間再執行
Future結果回撥then
final future = Future.delayed(Duration(seconds: 1), () {
print('進行計算');
return 4;
});
future.then((value) => print(value));
print('繼續進行接下來的任務');
// flutter: 繼續進行接下來的任務
// flutter: 進行計算
// flutter: 4
複製程式碼
Future出現錯誤後的回撥onError
final future = Future.error(3);
future.then((value) => print(value))
.onError((error, stackTrace) => print(error));
print('繼續進行接下來的任務');
// flutter: 繼續進行接下來的任務
// flutter: 3
複製程式碼
Future完成的回撥whenComplete
final future = Future.error(3);
future.then((value) => print(value))
.onError((error, stackTrace) => print(error))
.whenComplete(() => print("完成"));
print('繼續進行接下來的任務');
// flutter: 繼續進行接下來的任務
// flutter: 3
// flutter: 完成
複製程式碼
async/await
做過前端開發的對這兩個關鍵字應該很熟悉,Flutter中async/await
本質上只是Future
的語法糖,使用方法也很簡單。
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}
複製程式碼
await
放在返回值為Future
的執行任務前面,相當於做了個標記,表示執行到此為止,等有結果後再往下執行;- 使用了
await
必須在方法後面加上async
;async
方法必須在返回值上封裝上Future
。
FutureBuilder
Flutter
為我們封裝了一個FutureBuilder
這個Widget,可以方便的構造UI, 以獲取圖片進行展示為例:
FutureBuilder(
future: 載入圖片的Future
uilder: (context, snapshot) {
// 未完成
if (!snapshot.hasData) {
// 使用預設的佔點陣圖
} else if (snapshot.hasError) {
// 使用載入失敗的圖
} else {
// 使用載入到的圖
}
},
複製程式碼
總結
通過新建Isolate
可以實現多執行緒,每個執行緒Isolate
都有Event Loop可以執行非同步操作。
有些移動開發者可能偏愛ReactiveX響應式程式設計,譬如RxJava
,RxSwift
,ReactiveCocoa
等。其實他們也是非同步程式設計的一種方式,Flutter
為我們提供了一個對應的類---Stream
,其也有豐富的中間操作符,還提供了StreamBuilder
可以構建UI,接下來我們將會一篇文章來分析它。