跳至主要內容

並行處理與隔離

所有 Dart 程式碼都在隔離區中執行,隔離區類似於執行緒,但不同之處在於隔離區擁有各自獨立的記憶體。它們不會以任何方式共享狀態,並且只能透過傳遞訊息進行通訊。預設情況下,Flutter 應用程式的所有工作都在單一隔離區(主要隔離區)上完成。在大多數情況下,此模型可實現更簡單的程式設計,並且速度足以使應用程式的 UI 不會變得無回應。

但是,有時應用程式需要執行特別大量的運算,這可能會導致「UI 卡頓」(畫面抖動)。如果您的應用程式因為這個原因而出現卡頓,您可以將這些運算移至輔助隔離區。這允許底層執行環境與主 UI 隔離區的工作並行執行運算,並利用多核心裝置的優勢。

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

The main isolate diagram

為了實現流暢的渲染,Flutter 每秒將「繪製影格」事件添加到事件佇列 60 次(適用於 60Hz 裝置)。如果這些事件沒有及時處理,應用程式就會出現 UI 卡頓,甚至更糟,完全沒有回應。

Event jank diagram

每當某個程序無法在影格間隔(兩個影格之間的時間)內完成時,最好將工作卸載到另一個隔離區,以確保主要隔離區每秒可以產生 60 個影格。當您在 Dart 中產生一個隔離區時,它可以與主要隔離區並行處理工作,而不會阻塞它。

您可以在 Dart 文件中的並行處理頁面中閱讀更多有關隔離區和事件迴圈如何在 Dart 中運作的資訊。


隔離區和事件迴圈 | Flutter in Focus

隔離區的常見使用案例

#

何時應該使用隔離區只有一個硬性規則,那就是當大量的運算導致您的 Flutter 應用程式出現 UI 卡頓時。當任何運算所花費的時間長於 Flutter 的影格間隔時,就會發生這種卡頓。

Event jank diagram

任何程序都可能需要更長的時間才能完成,具體取決於實作方式和輸入資料,因此不可能建立何時需要考慮使用隔離區的詳盡清單。

也就是說,隔離區通常用於以下情況:

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

隔離區之間的訊息傳遞

#

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

當傳遞到隔離區時,唯一不會被複製的物件是不會改變的不可變物件,例如字串或不可修改的位元組。當您在隔離區之間傳遞不可變物件時,為了獲得更好的效能,會跨連接埠傳送對該物件的參考,而不是複製該物件。由於不可變物件無法更新,因此這有效地保留了 Actor 模型行為。

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

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

短暫的隔離區

#

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

Isolate diagram

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

例如,考慮以下程式碼,它會從檔案載入大型 JSON blob,並將該 JSON 轉換為自訂 Dart 物件。如果未將 json 解碼程序卸載到新的隔離區,此方法會導致 UI 變得無回應數秒。

dart
// 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 的完整逐步解說,請參閱此 cookbook 範例

具狀態、較長壽命的隔離區

#

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

為此,您可以使用一些 Isolate.run 抽象化的低層級隔離區相關 API

當您使用 Isolate.run 方法時,新的隔離區會在向主要隔離區傳回單一訊息後立即關閉。有時,您需要長時間存在的隔離區,並且可以隨著時間的推移相互傳遞多個訊息。在 Dart 中,您可以使用 Isolate API 和連接埠來實現此目的。這些長時間存在的隔離區俗稱背景工作程式

當您有需要在應用程式的整個生命週期中重複執行的特定程序,或者如果您有一個在一段時間內執行並且需要將多個傳回值傳回給主要隔離區的程序時,長時間存在的隔離區會很有用。

或者,您可以使用worker_manager來管理長時間存在的隔離區。

ReceivePort 和 SendPort

#

使用兩個類別(除了 Isolate 之外)設定隔離區之間長時間存在的通訊:ReceivePortSendPort。這些連接埠是隔離區可以相互通訊的唯一方式。

Ports 的行為類似於 Streams,其中 StreamControllerSink 是在一個隔離區中建立的,而接聽程式是在另一個隔離區中設定的。在此類比中,StreamConroller 稱為 SendPort,您可以使用 send() 方法「新增」訊息。ReceivePort 是接聽程式,當這些接聽程式收到新訊息時,它們會使用該訊息作為引數呼叫提供的回呼。

如需有關設定主要隔離區和工作隔離區之間雙向通訊的深入說明,請遵循Dart 文件中的範例。

在隔離區中使用平台外掛程式

#

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

平台通道隔離區使用 BackgroundIsolateBinaryMessenger API。以下程式碼片段顯示在背景隔離區中使用 shared_preferences 套件的範例。

dart
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,很自然會期望 Isolate 的行為類似執行緒,但事實並非如此。Isolate 有它們自己的全域欄位,而且只能透過訊息傳遞進行溝通,這確保了在一個 Isolate 中的可變物件只能在該 Isolate 中被存取。因此,Isolate 的存取權限受限於它們自己的記憶體。例如,如果你的應用程式有一個名為 configuration 的全域可變變數,它會被複製為一個新的全域欄位到新產生的 Isolate 中。如果你在新產生的 Isolate 中變更該變數,它在主 Isolate 中保持不變。即使你將 configuration 物件作為訊息傳遞到新的 Isolate,情況也是如此。這就是 Isolate 的運作方式,當你考慮使用 Isolate 時,請務必記住這一點。

Web 平台和運算

#

包括 Flutter web 在內的 Dart web 平台不支援 Isolate。如果你的 Flutter 應用程式以 web 為目標,你可以使用 compute 方法來確保你的程式碼能夠編譯。在 web 上,compute() 方法會在主執行緒上執行計算,但在行動裝置上則會產生新的執行緒。在行動和桌面平台上,await compute(fun, message) 等同於 await Isolate.run(() => fun(message))

有關 web 上並行的更多資訊,請查看 dart.dev 上的並行文件

無法存取 rootBundledart:ui 方法

#

所有的 UI 任務和 Flutter 本身都耦合到主 Isolate。因此,你無法在新產生的 Isolate 中使用 rootBundle 存取資源,也無法在新產生的 Isolate 中執行任何 widget 或 UI 工作。

從主機平台到 Flutter 的外掛程式訊息有限

#

透過背景 Isolate 平台通道,你可以在 Isolate 中使用平台通道向主機平台(例如 Android 或 iOS)發送訊息,並接收這些訊息的回應。但是,你無法接收來自主機平台未經請求的訊息。

舉例來說,你無法在背景 Isolate 中設定一個長時間運作的 Firestore 監聽器,因為 Firestore 使用平台通道向 Flutter 推送更新,這些更新是未經請求的。但是,你可以在背景中查詢 Firestore 以獲得回應。

更多資訊

#

有關 Isolate 的更多資訊,請查看以下資源

  • 如果你使用許多 Isolate,請考慮 Flutter 中的 IsolateNameServer 類別,或是 pub 套件,該套件為未使用 Flutter 的 Dart 應用程式複製了相同的功能。
  • Dart 的 Isolate 是 Actor 模型的實作。
  • isolate_agents 是一個抽象 Ports 的套件,可以更輕鬆地建立長時間運作的 Isolate。
  • 閱讀更多關於 BackgroundIsolateBinaryMessenger API 的公告