一、Flutter中的Event Loop
Flutter是有Dart語言開發的,與Android一樣是事件驅動的,在Android中的結構是Looper/Handler,相信熟悉Android開發的的同事對Looper循環器和Handler都非常深刻。但在Dart語言中也有自己的Event Loop,那Dart中的Event Loop是什麼樣的結構呢? 下面我們先通過一個簡單的流程圖來觀察和了解一下Dart中的Event Loop。
Dart處理事件的的流程,從流程圖中我們可以總結出:
- 首先處理MicroTask queue中的所有MicroTask任務
- 處理完所有的MicroTask後執行一條Event又回到了MicroTask
- 繼續循環執行MicroTask,知道所有的event處理完。
我們需要注意的是隻有當MicroTask都消費完之後纔開始執行Event事件,並且執行一條Event之後又循環執行MicroTask。依次反覆執行,知道所有的Event都執行完畢。瞭解了Dart中的Event Loop,那接下來我們看一下Dart中的異步任務。
二、Flutter/Dart的異步UI
Dart是單線程執行模型,支持Isolates(在另一個線程上運行Dart代碼的方式)、事件循環和異步編程。 除非您啓動一個Isolate,否則您的Dart代碼將在主UI線程中運行,並由事件循環驅動
比如我們可以在UI線程上運行網絡請求代碼而不會導致UI掛起(譯者語:因爲網絡請求是異步的):
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
要更新UI,您可以調用setState,這會觸發build方法再次運行並更新數據。
@override
void initState() {
// TODO: implement initState
super.initState();
loadData();
}
在這裏我們需要注意async 和await的用法
1、Async和await
async
和await
是什麼?它們是Dart語言的關鍵字,有了這兩個關鍵字,可以讓你用同步代碼的形式寫出異步代碼
我們首先看一個案例:
首先我們用async修飾一個方法,來處理髮雜的數學計算。代碼如下:
syncFibonacci(int n) async {
return n < 2 ? n: subtraction(n-1) ;
}
int subtraction(int num){
return (num-2) + (num-1);
}
然後我們在創建一個fun方法,調用syncFibonacci方法如下:
void fun(){
int num = await syncFibonacci(20);
print('$num')
}
需要注意的時我們調用的時候,必須添加await方法,才能順利執行出最終結果,如果我們不用await修飾的時候,就會包如下錯誤:
Unhandled Exception: type 'Future<dynamic>' is not a subtype of type 'int'
Future類型不匹配的錯誤,
那爲什麼會報Future類型不匹配的錯誤?因爲num是int類型,而函數syncFibonacci(20)是一個異步操作函數,其返回值是一個await延遲執行的結果。在Dart中,有await標記的運算,其結果值都是一個Future對象,Future不是int類型,所以就報錯了。所以爲了獲取到延遲執行的結果,Dart規定有async標記的函數,只能由await來調用。這樣我們就可以很好的處理返回結果了。
接下來我們在看另外一個案例:
void main() async{
print('main() run');
teach();
print('mian() end');
...
}
teach() async {
print(' teach() run');
String result = await food();
print('teach() $result');
}
food() async {
print("food() run");
return "hello";
}
附上我們在控制檯看到的結果:
從結果我們可以總結如下:
await
並不是程序運行到這裏等待Future
完成。而是立刻結束當前函數的執行並返回一個Future
。函數內剩餘代碼通過調度異步執行。
await
只能在async
函數中出現。async
函數中可以出現多個await
,每遇見一個就返回一個Future
, 實際結果類似於用then
串起來的回調。async
函數也可以沒有await
, 在函數體同步執行完畢以後返回一個Future
。
2、Future
我們首先看一下Future的創建,Future的創建可以通過一下四種方式創建
//通過new 直接創建Future
Future((){print('Future 創建');});
//創建一個Future,在duration後執行
Future.delayed(const Duration(seconds:1), (){print('在1秒後Future');});
//創建一個microtask中運行的future
Future.microtask((){print('在microtask中運行Future');});
//創建一個同步執行的future
Future.sync((){print('同步運行的Future');});
注意:Future的創建是在UI線程中執行的
有了Future的對象之後,我麼可以通過then串將傳遞過來的回調函數調度到任務隊列異步執行。
Future((){print('創建了Future對象');})
.then((_){print('執行函數一');})
...
.then((_){print('執行函數二');})
//用來捕獲異常,類似與Java中的try{}catch (Exception e){ System.out.println(e.getMessage()); }
.catchError((error)=>print('$error'))
//無論有沒有異常都會執行的whenComplete(),類似於try..catch..中的finally{}操作。
.whenComplete(()=> print('執行完成'));
通過Future對象之後,我麼執行then函數,可以是一個then函數,也可以是多個then函數,同時我們在請求網絡或文件讀寫等負責邏輯的時候,我們可以通過catchErro來捕獲異常,同時在whenComplete來關閉文件流,數據庫的遊標對象等操作。
需要注意的是:
- 你通過then串起來的那些回調函數在
Future創建
完成的時候會被立即執 行,也就是說它們是同步執行,而不是被調度異步執行。 - 如果
Future
在調用then
串起回調函數之前已經完成,那麼這些回調函數會被調度到microtask queue異步執行。 - 通過
Future()
和Future.delayed()
實例化的Future
不會同步執行,它們會被調度到事件隊列異步執行。 - 通過
Future.value()
實例化的Future
會被調度到microtask queue異步完成,類似於第2條。 - 通過
Future.sync()
實例化的Future
會同步執行其入參函數,然後(除非這個入參函數返回一個Future
)調度到microtask queue來完成自己,類似於第2條。
3、使用Isolate
在Flutter中,可以利用多個CPU內核來執行耗時或計算密集型任務。這是通過使用Isolates來完成的。是一個獨立的執行線程,它運行時不會與主線程共享任何內存。這意味着你不能從該線程訪問變量或通過調用setState來更新你的UI。那麼Isolate是如何來完成數據交互的呢?我們以計算斐波那契數列爲例
首先分裝一個函數,用來計算斐波那契數列值
int syncFibonacci(int n) {
return n < 2 ? n : syncFibonacci(n - 2) + syncFibonacci(n - 1);
}
然後我們採用Isolate來計算n的值
*方法一,計算斐波那契數裏*/
void fun(int n) async {
/*
* 首先創建一個ReceivePort,爲什麼要創建這個?
* 因爲創建Isolate所需的參數,必須要有SendPort,SendPort需要ReceivePort來創建
* */
ReceivePort response = new ReceivePort();
/*
* 開始創建Isolate,通過Isolate調用spawn函數,將我們創建的_isolate傳遞過去。
* _isolate是創建Isolate必須要的參數
* */
await Isolate.spawn(_isolate, response.sendPort);
//獲取sendPort來發送參數
SendPort sendPort = await response.first;
//創建接收消息的ReceivePort
ReceivePort receivePort = new ReceivePort();
//發送數據
sendPort.send([n, receivePort.sendPort]);
/*接收Isolate處理完成程序返回的數據*/
int num = await receivePort.first;
print("============================$num");
}
//創建isolate必須要的參數
void _isolate(SendPort initReplyto) {
final ReceivePort port = new ReceivePort();
//綁定
initReplyto.send(port.sendPort);
//監聽
port.listen((messsage) {
//獲取數據並解析
final int data = messsage[0];
final SendPort send = messsage[1];
//返回結果
send.send(syncFibonacci(data));
});
}
需要注意的是是在UI和Isolate裏面使用ReceivePort進行雙向通信,那麼既然是雙向綁定,我們就可以不斷調用或者多次監聽返回。比如通過長連接獲取服務器端的數據,當服務器數據變化的時候,能夠及時更新UI中的變化。
4、 使用compute
對上述案例,如果使用compute處理的話,就變得比較簡單,
/*方法一,計算斐波那契數裏*/
fun() async {
int num = await compute(syncFibonacci, 20);
print('===============$num');
}
通過對比,我們就發現compute的使用比較簡單,因爲在dart中的Isolate比較重量級,UI線程和Isolate中的數據的傳輸比較複雜,因此flutter爲了簡化用戶代碼,在foundation庫中封裝了一個輕量級compute操作,
但要使用compute,必須注意的有兩點,
a、我們的compute中運行的函數,必須是頂級函數或者是static函數,
b、compute傳參,只能傳遞一個參數,一次調用返回值也只有一個:
其次compute的使用還是有些限制,它沒有辦法多次返回結果,也沒有辦法持續性的傳值計算,每次調用,相當於新建一個隔離,如果調用過多的話反而會適得其反。在有些些業務中我們可以使用compute,但是有些些業務中,我們只能使用dart提供的Isolate了,比如處理不斷監聽服務端返回的消息信息。