Flutter 假非同步

前行的烏龜發表於2019-10-30

Flutter 假非同步

就像 android 有 handle 一樣,訊息佇列這東西好像還真是系統必備,Flutter 也有自己的訊息佇列,只不過佇列直接封裝在了 Dart 的執行緒型別 Isolate 裡面了,不過 Flutter 還是提供了 Futrue 這個 API 來專門來操作各種訊息,以及實現基於訊息佇列的假非同步


Flutter 的“非同步”機制

這裡的非同步是加了引號的,可見此非同步非真非同步,而是假非同步。Flutter 的非同步不是開新執行緒,而是往所屬執行緒的 訊息佇列 中新增任務,當然大家也可以按上文那樣自己展開真非同步操作

Flutter 對程式碼分2類:同步程式碼和非同步程式碼

  • 同步程式碼:傳統一行行寫下來,一行行執行的程式碼
  • 非同步程式碼:通過 Future API 把任務新增到 Isolate 所屬訊息佇列執行的偽非同步
  • 執行順序:先執行同步程式碼,再執行非同步程式碼

為啥,很明顯啊,非同步程式碼是往訊息佇列裡新增任務,那肯定得等現在的程式碼執行完了,執行緒有空閒了才能開始執行訊息佇列裡的任務呀~

舉個例子:

void test() {
    print("AA");
    Future(() => print("Futrue"));
    print("BB");
}
~~~~~~~~~~~log~~~~~~~~~~~~~
I/flutter (10064): AA
I/flutter (10064): BB
I/flutter (10064): Futrue
複製程式碼

print("Futrue"))任務等到最後才執行的...

Flutter 提供了往 訊息佇列 新增資料的 API:Future

  • 往 MicroTask 佇列新增任務
scheduleMicrotask((){
  // ...code goes here...
}); 

new Future.microtask((){
    // ...code goes here...
});
複製程式碼
  • 往 Event 佇列新增任務
new Future(() {
  // ...code goes here...
});
複製程式碼

Future 的基本使用

Future 物件是 Flutter 專門提供的,基於訊息佇列實現非同步的類,Future 物件會把自身當做一個任務新增到訊息佇列中去排隊執行

Future 物件接受的是一個函式,就是要執行的任務,用() => ...簡寫也是可以的

void task() {
    print("AA");
}

var futrue = Future(task);
複製程式碼

建立 Future 任務方式:

  • Future()
  • Future.microtask()
  • Future.sync() - 同步任務
  • Future.value()
  • Future.delayed() - 延遲xx時間新增任務
  • Future.error() - 錯誤處理

我們來看幾個代表性的:

  • Future.sync() - 阻塞任務,會阻塞當前程式碼,sync 的任務執行完了,程式碼才能走到下一行
void test() {
    print("AA");
    Future.sync(() => print("Futrue"));
    print("BB");
}
~~~~~~~~~~~~log~~~~~~~~~~~~~~
I/flutter (10573): AA
I/flutter (10573): Futrue
I/flutter (10573): BB
複製程式碼
  • Future.delayed() - 延遲任務,指定xx時間後把任務新增到訊息佇列,要是訊息佇列前面有人執行的時間太長了,那麼執行時間點就不能把握了,這點大家要知道
void test() {
    print("AA");
    Future.delayed(Duration(milliseconds: 500),() => print("Futrue"));
    print("BB");
}
~~~~~~~~~~~~log~~~~~~~~~~~~~~
I/flutter (10573): AA
I/flutter (10573): BB
I/flutter (10573): Futrue
複製程式碼

Future 的鏈式呼叫

Future 也支援鏈式呼叫的,在 API 使用上也是很靈活的,提供了下面的選擇給大家

  • .then - 在 Future 執行完後執行,相當於一個 callback,而不是重新建立了一個 Future
    Future.delayed(Duration(seconds: 1),(){
      print(("AAA"));
      return "AA";
    }).then((value){
      print(value);
    });
複製程式碼
  • .catchError - future 不管在任何位置發生了錯誤,都會立即執行 catchError
    Future.delayed(Duration(seconds: 1),(){
      throw Exception("AAA");
    }).then((value){
      print(value);
    }).catchError((error){
      print(error);
    });
複製程式碼
  • .whenComplete - 不管是否發生異常,在執行完成後,都會執行該方法
    Future.delayed(Duration(seconds: 1), () {
      throw Exception("AAA");
    }).then((value) {
      print(value);
    }).catchError((error) {
      print(error);
    }).whenComplete(() {
      print("complete...");
    });
複製程式碼
  • .wait - 可以等待所有的 future 都執行完畢再走 then 的方法
    Future.wait([
      // 2秒後返回結果
      Future.delayed(new Duration(seconds: 2), () {
        return "hello";
      }),
      // 4秒後返回結果
      Future.delayed(new Duration(seconds: 4), () {
        return " world";
      })
    ]).then((results) {
      print(results[0] + results[1]);
    }).catchError((e) {
      print(e);
    });
複製程式碼

大家想想啊

Futrue()
    .then()
    .then()
    ...
複製程式碼

這樣的鏈式寫法不就是標準的去 callback 回撥地獄的方式嘛


async/await 關鍵字

async/await 這組關鍵字是系統提供的另一種實現非同步任務的 API,async/await 底層還是用 Futrue 實現的,從使用上看是對 Futrue 的簡化,本質上還是基於訊息佇列實現的非同步,是假非同步,和 Isoalte 是不一樣的

async/await 的特點就是:成對出現

  • async - 修飾方法,用 async 宣告的方法都是耗時的
  • await - 呼叫 async 方法時使用,也可以在 async 方法內部是適用,await 表示阻塞,下面的任務必須等 await 呼叫的方法執行完之後才能執行

比如這樣:

  anysncTest() async {
    print("async 休眠 start...");
    sleep(Duration(seconds: 1));
    print("async 休眠 end...");
  }

await anysncTest();
複製程式碼

本質上 await 呼叫的方法其實是把這個方法包裝到 Futrue 中去訊息佇列裡執行,只不過是:Future.sync() 阻塞式的 Future 任務

async 在佈局中也是可以直接用的

class TestWidgetState extends State<TestWidget> {
  int _count = 0;
 
  @override
  Widget build(BuildContext context) {
    return Material(
        FlatButton(
            onPressed: () async {
                _count = countEven(1000000000);
                setState(() {});
            },
            child: Text(
                _count.toString(),
            )),
    );
  }
複製程式碼

async/await 是阻塞式的函式

實驗1:
  // 這是非同步任務程式碼
  aaa() async{
    print("main1...");
    await anysncTest();
    print("main2...");
    print("main3...");
  }

  anysncTest() async {
    print("async 休眠 start...");
    sleep(Duration(seconds: 1));
    print("async 休眠 end...");
  }
  
  // 點選按鈕去執行
    Widget build(BuildContext context) {
    return RaisedButton(
      child: (Text("click!")),
      onPressed: () async  {
        await aaa();
      },
    );
  }
複製程式碼

Flutter 假非同步
可以看到 async/await 執行的方法的確是阻塞時的,至少在這個 async 方法裡絕對是阻塞式的

實驗2:

那麼範圍擴充套件一下,在 async 外面再來看看 async/await 是不是阻塞式的?有人說 async/await 和協程一樣,協程的關鍵點在於非競爭式資源,協程的概念中,當多個協程中有一個協程掛起之後,並不會阻塞 CPU,CPU 回去執行其他協程方法,直到有空閒了再來執行之前掛起後恢復的協程,雖然在協程看來我掛起了執行緒,但其實 CPU 不會被協程掛起阻塞,這點就是協程的核心優勢,大大提升多執行緒下的執行效率。

從這點出發我們就能知道 async/await 是不是又一個協程了,看看他阻塞 CPU,我們在 await 之後看看 async 後面的程式碼會不會執行就 OK了

  // 還是這組方法
  aaa() async{
    print("main1...");
    await anysncTest();
    print("main2...");
    print("main3...");
  }

  anysncTest() async {
    print("async 休眠 start...");
    sleep(Duration(seconds: 1));
    print("async 休眠 end...");
  }
  
  // 執行,注意此時按鈕的點選方法不是 async 的
  Widget build(BuildContext context) {
    return RaisedButton(
      child: (Text("click!")),
      onPressed: () {
        print("click1...");
        aaa();
        print("click2...");
        print("click3...");
      },
    );
  }
複製程式碼
I/flutter ( 5733): click1...
I/flutter ( 5733): main1...
I/flutter ( 5733): async 休眠 start...
I/flutter ( 5733): async 休眠 end...
I/flutter ( 5733): click2...
I/flutter ( 5733): click3...
I/flutter ( 5733): main2...
I/flutter ( 5733): main3...
複製程式碼

await 阻塞是真的阻塞 CPU 了,所以 async/await 不是協程,但是大家注意啊,在 await 結速阻塞之後執行的是 click2 也就是 async 外部的方法,說明 await 標記的方法返回的都是 Futrue 物件的說法是正確的,佇列只有線上程空閒時才會執行,顯然此時執行緒不是空閒的,點選方法還沒執行完呢

實驗3:

這次做對比實驗,把點選事件也變成 async 的看看執行順序

  // 還是這組方法
  aaa() async{
    print("main1...");
    await anysncTest();
    print("main2...");
    print("main3...");
  }

  anysncTest() async {
    print("async 休眠 start...");
    sleep(Duration(seconds: 1));
    print("async 休眠 end...");
  }
  
  // 執行
    Widget build(BuildContext context) {
    return RaisedButton(
      child: (Text("click!")),
      onPressed: () async {
        print("click1...");
        await aaa();
        print("click2...");
        print("click3...");
      },
    );
  }
複製程式碼
I/flutter ( 5733): click1...
I/flutter ( 5733): main1...
I/flutter ( 5733): async 休眠 start...
I/flutter ( 5733): async 休眠 end...
I/flutter ( 5733): main2...
I/flutter ( 5733): main3...
I/flutter ( 5733): click2...
I/flutter ( 5733): click3...
複製程式碼

這樣看的話在 async 方法內部,是嚴格按照順序執行的


async 方法的格式

1. async 標記的方法返回值都是 Futrue 型別的

上文書哦說 await 呼叫的方法返回的都是 Futrue 物件,那麼就是說在宣告 async 函式時,返回值都是 Futrue 型別的,Futrue 內部包裹實際的返回值型別

Futrue<String> getData() async {
  data = await http.get(Uri.encodeFull(url), headers: {"Accept": "application/json"});    
}
複製程式碼

Futrue<String> 我們可以不寫,dart 也會自動推斷出來,但是我們一定要知道是 Futrue 型別的,要不有時會報型別錯誤

我們在用的時候都是配合 await 使用的,這時候可以直接用具體型別值接返回值了

String data = await getData();
複製程式碼

記住:

Future就是event,很多Flutter內建的元件比如前幾篇用到的Http(http請求控制元件)的get函式、RefreshIndicator(下拉手勢重新整理控制元件)的onRefresh函式都是event。每一個被await標記的控制程式碼也是一個event,每建立一個Future就會把這個Future扔進event queue中排隊等候安檢~


Stream

StreamFuture 一樣都是假非同步操作,區別是 Stream 可以接受多次資料,我不詳細展開了,有待以後詳細研究

Stream.fromFutures([
  // 1秒後返回結果
  Future.delayed(new Duration(seconds: 1), () {
    return "hello 1";
  }),
  // 丟擲一個異常
  Future.delayed(new Duration(seconds: 2),(){
    throw AssertionError("Error");
  }),
  // 3秒後返回結果
  Future.delayed(new Duration(seconds: 3), () {
    return "hello 3";
  })
]).listen((data){
   print(data);
}, onError: (e){
   print(e.message);
},onDone: (){

});
複製程式碼

相關文章