Flutter異步編程詳解

不知道大家有沒有一個疑問: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
  • 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),
)
  • 修改代碼
  1. 新建一個calculatorByComputeFunction方法,用compute調用bigCompute方法:
void calculatorByComputeFunction() async {
    // 使用`compute`調用`bigCompute`方法,傳參0
    int result = await compute(bigCompute, 0);
    print(result);
}
  1. 修改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':.)

  1. 解決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間通信的原理如下:

  1. 當前Isolate接收其他Isolate消息的實現邏輯: Isolate之間是通過Port進行通信的,ReceivePort是接收器,它配套有一個SendPort發送器, 當前Isolate可以把SendPort發送器送給其他Isolate,其他Isolate通過這個SendPort發送器就可以發送消息給當前Isolate了。

  2. 當前Isolate給其他Isolate發消息的實現邏輯: 其他Isolate通過當前IsolateSendPort發送器發送一個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的官方介紹如下圖:

  • 靜態示意圖
Event Loop

執行完main()函數后將會創建一個Main Isolate

  • 動態示意圖

[圖片上傳失敗...(image-239e43-1622171612149)]

  • Event Loop會處理兩個隊列MicroTask queueEvent queue中的任務;
  • Event queue主要處理外部的事件任務:I/O,手勢事件,定時器,isolate間的通信等;
  • MicroTask queue主要處理內部的任務:譬如處理I/O事件的中間過程中可能涉及的一些特殊處理等;
  • 兩個隊列都是先進先出的處理邏輯,優先處理MicroTask queue的任務,當MicroTask queue隊列為空后再執行Event queue中的任務;
  • 當兩個隊列都為空的時候就進行GC操作,或者僅僅是在等待下個任務的到來。

為了比較好的理解 Event Loop 的異步邏輯,我們來打個比喻:就像我去長沙某網紅奶茶品牌店買杯“幽蘭拿鐵”(由于是現做的茶,比較耗時)的過程。

  1. 我來到前臺給服務員說我要買一杯你們店的“幽蘭拿鐵”,然后服務員遞給了我一個有編號的飛盤(獲取憑證);
  2. 奶茶店的備餐員工就將我的訂單放在訂單列表的最后面,他們按照順序準備訂單上的商品,準備好一個就讓顧客去領取(Event queue 先進先出進行處理),而我就走開了,該干啥干啥去了(異步過程,不等待處理結果);
  3. 突然他們來了個超級VIP會員的訂單,備餐員工就把這個超級VIP訂單放在了其他訂單的最前面,優先安排了這個訂單的商品(MicroTask優先處理)---此場景為虛構;
  4. 當我的訂單完成后,飛盤開始震動(進行結果回調),我又再次回到了前臺,如果前臺妹子遞給我一杯奶茶(獲得結果),如果前臺妹子說對不起先生,到您的訂單的時候沒水了,訂單沒法完成了給我退錢(獲得異常錯誤錯誤)。

我們常用的異步操作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.
});
// 繼續其他任務
...
  1. http.get('https://my.image.url')返回的是一個未完成狀態的Future, 可以理解為一個句柄,同時http.get('https://my.image.url')被丟進了Event queue中等待被執行,然后接著執行當前的其他任務;
  2. Event queue執行完這個get請求成功后會回調then方法,將結果返回,Future為完成狀態 ,就可以進行接下來的操作了;
  3. 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

做過前端開發的對這兩個關鍵字應該很熟悉,Flutterasync/await本質上只是Future的語法糖,使用方法也很簡單。

Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  return 'Your order is: $order';
}
  1. await放在返回值為Future的執行任務前面,相當于做了個標記,表示執行到此為止,等有結果后再往下執行;
  2. 使用了await必須在方法后面加上async;
  3. 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響應式編程,譬如RxJavaRxSwiftReactiveCocoa等。其實他們也是異步編程的一種方式,Flutter為我們提供了一個對應的類---Stream,其也有豐富的中間操作符,還提供了StreamBuilder可以構建UI,接下來我們將會一篇文章來分析它。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容