Flutter 非同步編程指南

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/04/04/17285937.html
-Advertisement-
Play Games

在 App 開發中,經常會遇到處理非同步任務的場景,如網路請求、讀寫文件等。Android、iOS 使用的是多線程,而在 Flutter 中為單線程事件迴圈 ...


作者:京東物流 王志明

1 Dart 中的事件迴圈模型

在 App 開發中,經常會遇到處理非同步任務的場景,如網路請求、讀寫文件等。Android、iOS 使用的是多線程,而在 Flutter 中為單線程事件迴圈,如下圖所示

Dart 中有兩個任務隊列,分別為 microtask 隊列和 event 隊列,隊列中的任務按照先進先出的順序執行,而 microtask 隊列的執行優先順序高於 event 隊列。在 main 方法執行完畢後,會啟動事件迴圈,首先將 microtask 隊列中的任務逐個執行完畢,再去執行 event 隊列中的任務,每一個 event 隊列中的任務在執行完成後,會再去優先執行 microtask 隊列中的任務,如此反覆,直到清空所有隊列,這個過程就是 Dart 事件迴圈的處理機制。這種機制可以讓我們更簡單的處理非同步任務,不用擔心鎖的問題。我們可以很容易的預測任務執行的順序,但無法準確的預測到事件迴圈何時會處理到你期望執行的任務。例如創建了一個延時任務,但排在前面的任務結束前是不會處理這個延時任務的,也就說這個任務的等待時間可能會大於指定的延遲時間。

Dart 中的方法一旦開始執行就不會被打斷,而 event 隊列中的事件還來自於用戶輸入、IO、定時器、繪製等,這意味著在兩個隊列中都不適合執行計算量過大的任務,才能保證流暢的 UI 繪製和用戶事件的快速響應。而且當一個任務的代碼發生異常時,只會打斷當前任務,後續任務不受影響,程式更不會退出。從上圖還可以看出,將一個任務加入 microtask 隊列,可以提高任務優先順序,但是一般不建議這麼做,除非比較緊急的任務並且計算量不大,因為 UI 繪製和處理用戶事件是在 event 事件隊列中的,濫用 microtask 隊列可能會影響用戶體驗。

總結下 Dart 事件迴圈的主要概念:

  1. Dart 中有兩個隊列來執行任務:microtask 隊列和 event 隊列。
  2. 事件迴圈在 main 方法執行完畢後啟動, microtask 隊列中的任務會被優先處理。
  3. microtask 隊列只處理來自 Dart 內部的任務,event 隊列中有來自 Dart 內部的 Future、Timer、isolate message,還有來自系統的用戶輸入、IO、UI 繪製等外部事件任務。
  4. Dart 中的方法執行不會被打斷,因此兩個隊列中都不適合用來執行計算量大的任務。
  5. 一個任務中未被處理的異常只會打斷當前任務,後續任務不受影響,程式更不會退出。

1.1 向 microtask 隊列中添加任務

可以使用頂層方法 scheduleMicrotask 或者 Future.microtask 方法,如下所示:

scheduleMicrotask(() => print('microtask1'));
Future.microtask(() => print('microtask2'));

使用 Future.microtask 的優勢在於可以在 then 回調中處理任務返回的結果。

1.2 向 event 隊列中添加任務

Future(() => print('event task'));

基於以上理論,通過如下代碼可以驗證 Dart 的事件迴圈機制:

void main() {
  print('main start');

  Future(() => print('event task1'));

  Future.microtask(() => print('microtask1'));

  Future(() => print('event task1'));

  Future.microtask(() => print('microtask2'));

  print('main stop');

執行結果:

main start
main stop
microtask1
microtask2
event task1
event task1

通過輸出結果可以看到,任務的執行順序並不是按照編寫代碼的順序來的,將任務添加到隊列不會立刻執行,而執行順序也完全符合前面講的規則,當前 main 方法中的代碼執行完畢後,才會去執行隊列中的任務,且 microTask 隊列的優先順序高於 event 隊列。

2 Dart 中的非同步實現

在 Dart 中通過 Future 來執行非同步任務, Future 是對非同步任務狀態的封裝,對任務結果的代理,通過 then 方法可以註冊處理任務結果的回調方法。

創建方法 Future 方式:
Future()
Future.delayed()
Future.microtask()
Future.sync()

2.1 Future()

factory Future(FutureOr<T> computation()) {
  _Future<T> result = new _Future<T>();
  Timer.run(() {
    try {
      result._complete(computation());
    } catch (e, s) {
      _completeWithErrorCallback(result, e, s);
    }
  });
  return result;
}

上面是 Future() 的源碼,可以看到內部是通過啟動一個沒有延遲的計時器來添加任務的,實用 try catch 來捕獲任務代碼中可能出現的異常,我們可以在 catchError 回調中來處理異常。

2.2 Future.delayed()

factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
  if (computation == null && !typeAcceptsNull<T>()) {
    throw ArgumentError.value(null, "computation", "The type parameter is not nullable");
  }
  _Future<T> result = new _Future<T>();
  new Timer(duration, () {
    if (computation == null) {
      result._complete(null as T);
    } else {
      try {
        result._complete(computation());
      } catch (e, s) {
        _completeWithErrorCallback(result, e, s);
      }
    }
  });
  return result;
}

Future.delayed() 與 Future() 的區別是通過一個延遲的計時器來添加任務。

2.3 Future.microtask()

factory Future.microtask(FutureOr<T> computation()) {
  _Future<T> result = new _Future<T>();
  scheduleMicrotask(() {
    try {
      result._complete(computation());
    } catch (e, s) {
      _completeWithErrorCallback(result, e, s);
    }
  });
  return result;
}

Future.microtask() 是將任務添加到 microtask 隊列,通過這種可以很方便通過 then 方法中的回調來處理任務的結果。

2.4 Future.sync()

factory Future.sync(FutureOr<T> computation()) {
  try {
    var result = computation();
    if (result is Future<T>) {
      return result;
    } else {
      // TODO(40014): Remove cast when type promotion works.
      return new _Future<T>.value(result as dynamic);
    }
  } catch (error, stackTrace) {
    var future = new _Future<T>();
    AsyncError? replacement = Zone.current.errorCallback(error, stackTrace);
    if (replacement != null) {
      future._asyncCompleteError(replacement.error, replacement.stackTrace);
    } else {
      future._asyncCompleteError(error, stackTrace);
    }
    return future;
  }
}

Future.sync() 中的任務會被立即執行,不會添加到任何隊列。

在第一個章節中講到了可以很容易的預測任務的執行順序,下麵我們通過一個例子來驗證:

void main() {
  print('main start');

  Future.microtask(() => print('microtask1'));

  Future.delayed(new Duration(seconds:1), () => print('delayed event'));
  Future(() => print('event1'));
  Future(() => print('event2'));

  Future.microtask(() => print('microtask2'));

  print('main stop');
}

執行結果:

main start
main stop
microtask1
microtask2
event1
event2
delayed event

因為代碼比較簡單,通過代碼可以很容易的預測到執行結果,下麵將複雜度稍微提高。

void main() {
  print('main start');

  Future.microtask(() => print('microtask1'));

  Future.delayed(new Duration(seconds:1), () => print('delayed event'));

  Future(() => print('event1'))
    .then((_) => print('event1 - callback1'))
    .then((_) => print('event1 - callback2'));

  Future(() => print('event2')).then((_) {
    print('event2 - callback1');
    return Future(() => print('event4')).then((_) => print('event4 - callback'));
  }).then((_) {
    print('event2 - callback2');
    Future(() => print('event5')).then((_) => print('event5 - callback'));
  }).then((_) {
    print('event2 - callback3');
    Future.microtask(() => print('microtask3'));
  }).then((_) {
    print('event2 - callback4');
  });

  Future(() => print('event3'));

  Future.sync(() => print('sync task'));

  Future.microtask(() => print('microtask2')).then((_) => print('microtask2 - callbak'));

  print('main stop');
}

執行結果:

main start
sync task
main stop

microtask1
microtask2
microtask2 - callbak

event1
event1 - callback1
event1 - callback2

event2
event2 - callback1
event3

event4
event4 - callback

event2 - callback2
event2 - callback3
event2 - callback4

microtask3
event5
event5 - callback

delayed event

看到結果後你可能會疑惑,為什麼 event1、event1 - callback1、event1 - callback2 會連續輸出,而 event2 - callback1 輸出後為什麼是 event3,event5、event5 - callback 為什麼會在 microtask3 後輸出?

這裡我們補充下 then 方法的一些關鍵知識,理解了這些,上面的輸出結果也就很好理解了:

  1. then 方法中的回調並不是按照它們註冊的順序來執行。
  2. Future 中的任務執行完畢後會立刻執行 then 方法中的回調,並且回調不會被添加到任何隊列中。
  3. 如果 Future 中的任務在 then 方法調用之前已經執行完畢了,那麼會有一個任務被加入到 microtask 隊列中。這個任務執行的就是被傳入then 方法中的回調。

2.5 catchError、whenComplete

Future(() {
  throw 'error';
}).then((_) {
  print('success');
}).catchError((error) {
  print(error);
}).whenComplete(() {
  print('completed');
});

輸出結果:

error
completed

通過 catchError 方法註冊的回調,可以用來處理任務代碼產生的異常。不管 Future 中的任務執行成功與否,whenComplete 方法都會被調用。

2.6 async、await

使用 async、await 能以更簡潔的編寫非同步代碼,是 Dart 提供的一個語法糖。使用 async 關鍵字修飾的方法返回值類型為 Future,在 async 方法內可以使用 await 關鍵字來修飾非同步任務,在方法內部達到同步執行的效果,可以達到簡化代碼和提高可讀性的效果,不過如果想要處理異常,需要實用 try catch 語句來包裹 await 修飾的非同步任務。

void main() async {
  print(await getData());
}

Future<int> getData() async {
  final a = await Future.delayed(Duration(seconds: 1), () => 1);
  final b = await Future.delayed(Duration(seconds: 1), () => 1);
  return a + b;
}

3 Isolate介紹

前面講到耗時任務不適合放到 microtask 隊列或 event 隊列中執行,會導致 UI 卡頓。那麼在 Flutter 中有沒有既可以執行耗時任務又不影響 UI 繪製呢,其實是有的,前面提到 microtask 隊列和 event 隊列是在 main isolate 中運行的,而 isolate 是線上程中運行的,那我們開啟一個新的 isolate 就可以了,相當於開啟一個新的線程,使用多線程的方式來執行任務,Flutter 也為我們提供了相應的 Api。

3.1 compute

void main() async {
  compute<String, String>(
    getData,
    'Alex',
  ).then((result) {
    print(result);
  });
}

String getData(String name) {
  // 模擬耗時3秒
  sleep(Duration(seconds: 3));
  return 'Hello $name';
}

compute 第一個參數是要執行的任務,第二個參數是要向任務發送的消息,需要註意的是第一個參數只支持頂層參數。使用 compute() 可以方便的執行耗時任務,但是濫用的話也會適得其反,因為每次調用,相當於新建一個 isolate。上面的代碼執行一個經歷了 isolate 的創建以及銷毀過程,還有數據的傳遞會經歷兩次拷貝,因為 isolate 之間是完全隔離的,不能共用記憶體,整個過程除去任務本身的執行時間,也會非常的耗時,isolate 的創建也比較消耗記憶體,創建過多的 isolate 還有 OOM 的風險。這時我們就需要一個更優的解決方案,減少頻繁創建銷毀 isolate 所帶來的消耗,最好是能創建一個類似於線程池的東西,只要提前初始化好,後面就可以隨時使用,不用擔心會發生前面所講的問題,這時候 LoadBalancer 就派上用場了

3.2 LoadBalancer

// 用來創建 LoadBalancer
Future<LoadBalancer> loadBalancerCreator = LoadBalancer.create(2, IsolateRunner.spawn);

// 全局可用的 loadBalancer
late LoadBalancer loadBalancer;

void main() async {
  // 初始化 LoadBalancer
  loadBalancer = await loadBalancerCreator;

  // 使用 LoadBalancer 執行任務
  final result = await loadBalancer.run<String, String>(getData, 'Alex');
  print(result);
}

String getData(String name) {
  // 模擬耗時3秒
  sleep(Duration(seconds: 3));
  return 'Hello $name';
}

使用 LoadBalancer.create() 方法可以創建出一個 isolate 線程池,能夠指定 isolate 的數量,並自動實現了負載均衡。應用啟動後在合適的時機將其初始化好,後續就有一個全局可用的 LoadBalancer 了。

4 實用經驗

4.1 指定任務的執行順序

在開發中經常會有需要連續執行非同步任務的場景,例如下麵的例子,後面的一步任務直接需要以來前面任務的結果,所有任務正常執行完畢才算成功。

void main() async {
  print(await getData());
}

Future<int> getData() {
  final completer = Completer<int>();
  int value = 0;

  Future(() {
    return 1;
  }).then((result1) {
    value += result1;
    return Future(() {
      return 2;
    }).then((result2) {
      value += result2;
      return Future(() {
        return 3;
      }).then((result3) {
        value += result3;
        completer.complete(value);
      });
    });
  });

  return completer.future;
}

這種方式出現了回調地獄,代碼非常難以閱讀,實際開發中還會有處理異常的代碼,會顯得更加臃腫,編寫難度也大,顯然這種方式是不建議使用的。

4.2 使用 then 的鏈式調用

void main() async {
  print(await getData());
}

Future<int> getData() {
  int value = 0;
  return Future(() => 1).then((result1) {
    value += result1;
    return Future(() => 2);
  }).then((result2) {
    value += result2;
    return Future(() => 3);
  }).then((result3) {
    value += result3;
    return value;
  });
}

回調地獄的問題解決了,代碼可讀性提高很多。

4.3 使用 async、await

void main() async {
  print(await getData());
}


Future<int> getData() async {
  int value = 0;

  value += await Future(() => 1);
  value += await Future(() => 2);
  value += await Future(() => 3);

  return value;
}

效果顯而易見,代碼更加清晰了。

4.4 取消任務

在前面講到了 Dart 方法執行時是不能被中斷的,這就意味著一個 Future 任務開始後必然會走到完成的狀態,但是很多時候我們需要又取消一個非同步任務,唯一的辦法就是在任務結束後不執行回調代碼,就可以實現類似取消的效果。

4.5 CancelableOperation

在 Flutter 的 async 包中,提供了一個 CancelableOperation 給我們使用,使用它可以很簡單的實現取消任務的需求。

void main() async {
  // 創建一個可以取消的任務
  final cancelableOperation = CancelableOperation.fromFuture(
    Future(() async {
      print('start');
      await Future.delayed(Duration(seconds: 3)); // 模擬耗時3秒
      print('end');
    }),
    onCancel: () => print('cancel...'),
  );

  // 註冊任務結束後的回調
  cancelableOperation.value.then((val) {
    print('finished');
  });

  // 模擬1秒後取消任務
  Future.delayed(Duration(seconds: 1)).then((_) => cancelableOperation.cancel());
}

CancelableOperation 是對 Future 的代理, 對 Future 的 then 進行了接管,判斷 isCanceled 標記決定是否需要執行用戶提供的回調。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.創建一個新的WinForms或WPF應用程式,具體取決於您的需要。 2.將Telerik Reporting組件添加到您的應用程式中。您可以通過NuGet包管理器來完成此操作。 3.在您的應用程式中添加多個報表文件。您可以使用Telerik Report Designer創建報表並將其添加到您的 ...
  • 【目錄】 1 委托 2 事件-概念的引出 3 事件-關於異常 4 事件-關於非同步 5 委托-Func與Action 1 委托 在.NET中定義“委托”需要用到delegate關鍵字,它是存有對某個方法的引用的一種引用類型變數,類似於 C 或 C++ 中函數的指針。“委托”主要有兩大作用: (1)將方 ...
  • 一:背景 1. 講故事 昨天有位朋友找到我,說他的程式記憶體存在泄露導致系統特別卡,大地址也開了,讓我幫忙看一下怎麼回事?今天上午看了下dump,感覺挺有意思,在我的分析之旅中此類問題也蠻少見,算是完善一下體系吧。 二:WinDbg 分析 1. 到底是哪裡的泄露 在.NET高級調試訓練營中,我多次告訴 ...
  • linux的crypt 最近學校佈置了一個網安的小作業,要用到linux裡面的這個crypt函數,寫一篇總結一下。首先我們要瞭解這個函數是用來做什麼的。 密碼影子文件中存儲了每一個用戶的用戶明文和其單向哈希過的秘文 cipher = "$1$C68vnJ27$1ttFZ1/Rylq/xi350A0N ...
  • Redhat7/CentOS7 網路配置與管理(nmtui、nmcli、GNOME GUI、ifcfg文件、IP命令) 背景:作為系統管理員,需要經常處理主機網路問題,而配置與管理網路的方法和工具也有好幾種,這裡整理分享一下網路配置與管理的幾種方式。 1、NetworkManager 概述 在 Re ...
  • MPU6050(三軸MEMS陀螺儀 + 三軸MEMS加速度計 + 數字運動處理器DMP)把姿態解算出來的慣性數據和歐拉角,上報給Processing IDE,或上報匿名上位機(V7),從而實時聯動系統無人機模型的飛行姿態。下麵講一下整個聯調過程以及遇到的坑。 ...
  • 資料庫應用設計與開發實例 第一節 需求描述與分析 在此,結合某高校個性化課程線上選課的實際需求,給出一個簡化的需求分析 一、功能性需求 1 管理員後臺模塊 學生信息管理 教師信息管理 課程信息管理 班級信息管理 2 學生使用模塊 查詢課程 瀏覽所選課程 查詢成績 3 教師使用模塊 我的課程 登分 二 ...
  • 前言:本文是對這篇博客What is the endian format in Oracle databases?[1]的翻譯,如有翻譯不當的地方,敬請諒解,請尊重原創和翻譯勞動成果,轉載的時候請註明出處。謝謝! 英文地址:https://dbtut.com/index.php/2019/06/27 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...