並行性和分離

所有 Dart 程式碼都會在隔離區中執行,隔離區類似於執行緒,但不同之處在於隔離區有自己獨立的記憶體。它們不會以任何方式共用狀態,只能透過訊息傳遞進行通訊。預設情況下,Flutter 應用程式會在單一隔離區(主隔離區)中執行所有工作。在大部分情況下,這種模式允許進行更簡單的程式設計,而且執行速度夠快,不會造成應用程式的 UI 沒有回應。

不過,有時候應用程式需要執行異常龐大的運算,可能會導致「UI 延遲」(不流暢的動作)。如果你的應用程式因此出現延遲,你可以將這些運算移到一個輔助隔離區。這樣一來,底層執行時間環境就能與主 UI 隔離區的工作並行執行運算,並利用多核心裝置的優勢。

每個隔離區都有自己的記憶體和事件迴圈。事件迴圈會依據事件加入事件佇列的順序處理事件。在主隔離區中,這些事件可以是從處理使用者在 UI 中點選,到執行函式,再到在螢幕上繪製畫面。下圖顯示一個範例事件佇列,其中有 3 個事件等待處理。

The main isolate diagram

為了順暢地進行渲染,Flutter 會以每秒 60 次的速度(對於 60Hz 的裝置)將「繪製畫面」事件加入事件佇列。如果這些事件沒有即時處理,應用程式就會出現 UI 延遲,甚至更糟的是,完全沒有回應。

Event jank diagram

每當一個處理程序無法在畫面間隔(兩個畫面之間的時間)內完成時,最好將工作卸載到另一個隔離區,以確保主隔離區能產生每秒 60 個畫面。當你在 Dart 中產生一個隔離區時,它可以在不阻擋主隔離區的情況下,與主隔離區並行處理工作。

你可以進一步在 Dart 文件的並行處理頁面中了解隔離區和事件迴圈在 Dart 中的運作方式。

隔離區的常見使用案例

只有一個硬性規定說明何時應該使用 Isolate,那就是當大型運算導致你的 Flutter 應用程式出現 UI 延遲。當任何運算花費的時間超過 Flutter 的畫面間隔時,就會發生這種延遲。

Event jank diagram

任何程序都可能花費較長的時間才能完成,具體取決於實作和輸入資料,因此無法建立一個詳盡的清單,說明何時需要考慮使用 Isolate。

話雖如此,Isolate 通常用於以下情況:

  • 從本地資料庫讀取資料
  • 傳送推播通知
  • 剖析和解碼大型資料檔案
  • 處理或壓縮照片、音訊檔和影片檔
  • 轉換音訊和影片檔
  • 使用 FFI 時需要非同步支援
  • 對複雜清單或檔案系統套用篩選

Isolate 之間的訊息傳遞

Dart 的 Isolate 是 Actor 模型 的實作。它們只能透過訊息傳遞彼此通訊,而訊息傳遞是透過 Port 物件 進行的。當訊息在彼此之間「傳遞」時,它們通常會從傳送 Isolate 複製到接收 Isolate。這表示傳遞給 Isolate 的任何值,即使在該 Isolate 上變異,也不會變更原始 Isolate 上的值。

傳遞給 Isolate 時不會複製的唯一 物件 是無法變更的不可變物件,例如字串或不可變更的位元組。當你在 Isolate 之間傳遞不可變物件時,會傳送對該物件的參考,而不是複製物件,以提升效能。由於不可變物件無法更新,因此這有效地保留了 Actor 模型行為。

這個規則的例外情況是當一個隔離區在使用 Isolate.exit 方法傳送訊息時退出。由於傳送的隔離區在傳送訊息後將不再存在,因此它可以將訊息的所有權從一個隔離區傳遞到另一個隔離區,確保只有一個隔離區可以存取訊息。

傳送訊息的兩個最低層級基元是 SendPort.send,它在傳送時複製一條可變訊息,以及 Isolate.exit,它傳送訊息的參考。 Isolate.runcompute 都在底層使用 Isolate.exit

短暫隔離區

在 Flutter 中將程序移到隔離區的最簡單方法是使用 Isolate.run 方法。此方法會產生一個隔離區,將回呼傳遞到產生的隔離區以啟動一些運算,從運算中傳回一個值,然後在運算完成時關閉隔離區。這一切都與主隔離區同時發生,並且不會阻擋它。

Isolate diagram

Isolate.run 方法需要一個單一參數,即在新的隔離區上執行的回呼函數。此回呼的函數簽章必須只有一個必需的未命名參數。當運算完成時,它會將回呼的值傳回主隔離區,並退出產生的隔離區。

例如,考慮這段從檔案載入大型 JSON 資訊塊,並將該 JSON 轉換為自訂 Dart 物件的程式碼。如果 JSON 解碼程序未卸載至新的隔離區,此方法將導致使用者介面在數秒內沒有回應。

// Produces a list of 211,640 photo objects.
// (The JSON file is ~20MB.)
Future<List<Photo>> getPhotos() async {
  final String jsonString = await rootBundle.loadString('assets/photos.json');
  final List<Photo> photos = await Isolate.run<List<Photo>>(() {
    final List<Object?> photoData = jsonDecode(jsonString) as List<Object?>;
    return photoData.cast<Map<String, Object?>>().map(Photo.fromJson).toList();
  });
  return photos;
}

如需使用隔離區在背景中剖析 JSON 的完整演練,請參閱 這份食譜

有狀態、較長駐的隔離區

短暫的隔離區很方便使用,但產生新的隔離區以及將物件從一個隔離區複製到另一個隔離區需要效能開銷。如果您使用 Isolate.run 重複執行相同的運算,則透過建立不會立即結束的隔離區,您可能會獲得更好的效能。

為執行此操作,您可以使用 Isolate.run 抽象的少數較低層級的與隔離區相關的 API

當您使用 Isolate.run 方法時,新的隔離區會在傳回單一訊息給主要隔離區後立即關閉。有時,您需要長駐的隔離區,並且可以隨著時間推移互相傳遞多則訊息。在 Dart 中,您可以使用隔離區 API 和埠來完成此操作。這些長駐的隔離區俗稱為背景工作執行器

當您有一個特定程序需要在應用程式的生命週期中重複執行,或者您有一個程序會執行一段時間並需要傳回多個回傳值給主要 Isolate 時,長駐 Isolate 會很有用。

或者,您可以使用 worker_manager 來管理長駐 Isolate。

接收埠和傳送埠

使用兩個類別(除了 Isolate 之外)在 Isolate 之間建立長駐通訊:ReceivePortSendPort。這些埠是 Isolate 之間唯一可以互相通訊的方式。

的行為類似於 串流,其中 StreamControllerSink 會在一個 Isolate 中建立,而監聽器會在另一個 Isolate 中設定。在此類比中,StreamConroller 稱為 SendPort,您可以使用 send() 方法來「新增」訊息。 ReceivePort 是監聽器,當這些監聽器收到新訊息時,它們會呼叫提供的回呼,並將訊息作為引數傳入。

如需有關在主要 Isolate 和工作 Isolate 之間設定雙向通訊的詳細說明,請參閱 Dart 文件 中的範例。

在 Isolate 中使用平台外掛程式

在 Flutter 3.7 中,您可以在背景分離執行緒中使用平台外掛程式。這開啟了許多可能性,可以將繁重的、與平台相關的運算卸載到不會阻擋 UI 的分離執行緒中。例如,假設您使用原生主機 API(例如 Android 上的 Android API、iOS 上的 iOS API 等)加密資料。先前,將資料封送至主機平台可能會浪費 UI 執行緒時間,而現在可以在背景分離執行緒中完成。

平台通道分離執行緒使用 BackgroundIsolateBinaryMessenger API。下列程式碼片段顯示在背景分離執行緒中使用 shared_preferences 套件的範例。

import 'dart:isolate';

import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  // Identify the root isolate to pass to the background isolate.
  RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
  Isolate.spawn(_isolateMain, rootIsolateToken);
}

Future<void> _isolateMain(RootIsolateToken rootIsolateToken) async {
  // Register the background isolate with the root isolate.
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);

  // You can now use the shared_preferences plugin.
  SharedPreferences sharedPreferences = await SharedPreferences.getInstance();

  print(sharedPreferences.getBool('isDebug'));
}

分離執行緒的限制

如果您從支援多執行緒的語言轉換為 Dart,您可能會合理地預期分離執行緒的行為類似於執行緒,但事實並非如此。分離執行緒有自己的全域變數,而且只能透過訊息傳遞進行通訊,確保分離執行緒中的可變物件只能在單一分離執行緒中存取。因此,分離執行緒受到其存取自身記憶體的限制。例如,如果您有一個應用程式有一個稱為 configuration 的全域可變變數,它會在衍生的分離執行緒中複製為新的全域變數。如果您在衍生的分離執行緒中變更該變數,它在主分離執行緒中仍保持不變。即使您將 configuration 物件作為訊息傳遞給新的分離執行緒,情況仍然如此。這是分離執行緒預期的運作方式,在您考慮使用分離執行緒時,務必記住這一點。

網頁平台和運算

包含 Flutter 網頁的 Dart 網頁平台不支援隔離。如果您要透過 Flutter 應用程式鎖定網頁,您可以使用 compute 方法以確保您的程式碼編譯。 compute() 方法在網頁上執行主執行緒的運算,但在行動裝置上產生新的執行緒。在行動裝置和桌上型電腦平台上,await compute(fun, message) 等於 await Isolate.run(() => fun(message))

如需有關網頁並行運算的更多資訊,請查看 dart.dev 上的 並行運算文件

rootBundle 存取權或 dart:ui 方法

所有 UI 任務和 Flutter 本身都與主要隔離結合。因此,您無法在產生的隔離中使用 rootBundle 存取資源,您也不能在產生的隔離中執行任何小工具或 UI 工作。

從主機平台傳送至 Flutter 的外掛程式訊息有限

透過背景隔離平台頻道,您可以在隔離中使用平台頻道傳送訊息至主機平台(例如 Android 或 iOS),並接收對這些訊息的回應。但是,您無法從主機平台接收未經請求的訊息。

舉例來說,您無法在背景隔離中設定長期的 Firestore 偵聽器,因為 Firestore 使用平台頻道將更新推播至 Flutter,而這些更新是未經請求的。不過,您可以在背景中查詢 Firestore 以取得回應。

更多資訊

如需瞭解有關隔離的更多資訊,請查看下列資源

  • 如果您使用許多隔離,請考慮使用 Flutter 中的 IsolateNameServer 類別,或使用複製功能的 pub 套件,以供未採用 Flutter 的 Dart 應用程式使用。
  • Dart 的隔離是 Actor 模型 的實作。
  • isolate_agents 是封裝埠的套件,讓建立長駐式隔離變得更簡單。
  • 進一步瞭解 BackgroundIsolateBinaryMessenger API 公告