Flutter async、await、Future

一、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

asyncawait是什麼?它們是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了,比如處理不斷監聽服務端返回的消息信息。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章