併發與隔離區
所有 Dart 程式碼都在 Isolate 中執行,Isolate 類似於執行緒,但不同之處在於 Isolate 擁有自己的獨立記憶體。它們不以任何方式共享狀態,並且只能透過訊息傳遞進行通訊。預設情況下,Flutter 應用在單個 Isolate(主 Isolate)上執行所有工作。在大多數情況下,這種模型允許更簡單的程式設計,並且速度足夠快,應用程式的 UI 不會變得無響應。
然而,有時應用程式需要執行非常大的計算,這可能導致“UI 卡頓”(卡頓的動畫)。如果您的應用因此出現卡頓,您可以將這些計算移至輔助 Isolate。這允許底層執行時環境與主 UI Isolate 的工作並行執行計算,並利用多核裝置。
每個 Isolate 都有自己的記憶體和事件迴圈。事件迴圈按事件新增到事件佇列的順序處理事件。在主 Isolate 上,這些事件可以是任何內容,從處理使用者在 UI 中的點選、執行函式到在螢幕上繪製幀。下圖顯示了一個包含 3 個待處理事件的示例事件佇列。

為了實現流暢的渲染,Flutter 每秒會將一個“繪製幀”事件新增到事件佇列 60 次(對於 60Hz 裝置)。如果這些事件未及時處理,應用程式就會出現 UI 卡頓,甚至變得完全無響應。

每當一個程序無法在幀間隔(兩個幀之間的時間)內完成時,最好將工作分載到另一個 Isolate,以確保主 Isolate 每秒能夠產生 60 幀。當您在 Dart 中建立 Isolate 時,它可以與主 Isolate 並行處理工作,而不會阻塞它。
您可以在 Dart 文件的 併發頁面 上閱讀更多關於 Isolate 和事件迴圈如何在 Dart 中工作的資訊。
在 YouTube 新標籤頁觀看:“Isolates and the event loop | Flutter in Focus”
Isolate 的常見用例
#應該何時使用 Isolate 只有一個硬性規定,那就是當大型計算導致您的 Flutter 應用程式出現 UI 卡頓時。當任何計算花費的時間超過 Flutter 的幀間隔時,就會發生這種卡頓。

任何程序都可能因為實現和輸入資料而花費更長的時間來完成,這使得建立何時需要考慮使用 Isolate 的詳盡列表變得不可能。
也就是說,Isolate 通常用於以下方面:
- 從本地資料庫讀取資料
- 傳送推送通知
- 解析和解碼大型資料檔案
- 處理或壓縮照片、音訊檔案和影片檔案
- 轉換音訊和影片檔案
- 在使用 FFI 時需要非同步支援時
- 對複雜列表或檔案系統應用過濾
Isolate 之間的訊息傳遞
#Dart 的 Isolate 是 Actor 模型 的實現。它們只能透過訊息傳遞進行通訊,而訊息傳遞是透過 Port 物件 完成的。當訊息在 Isolate 之間“傳遞”時,通常會從傳送 Isolate 複製到接收 Isolate。這意味著傳遞給 Isolate 的任何值,即使在該 Isolate 中被修改,也不會改變原始 Isolate 中的值。
當傳遞給 Isolate 的物件中,唯一不被複制的是那些本身就無法更改的不可變物件,例如 String 或不可修改的位元組。當您在 Isolate 之間傳遞不可變物件時,會發送指向該物件的引用,而不是複製物件,以提高效能。因為不可變物件無法更新,這有效地保留了 Actor 模型行為。
這條規則的一個例外是,當 Isolate 使用 Isolate.exit 方法傳送訊息時退出。由於傳送 Isolate 在傳送訊息後將不復存在,因此它可以將訊息的所有權從一個 Isolate 轉移到另一個 Isolate,確保只有一個 Isolate 可以訪問該訊息。
傳送訊息的兩個最低級別的原語是 SendPort.send,它在傳送時會複製可變訊息,以及 Isolate.exit,它會發送訊息的引用。Isolate.run 和 compute 都使用 Isolate.exit。
短暫的 Isolate
#在 Flutter 中將程序移到 Isolate 的最簡單方法是使用 Isolate.run 方法。此方法會建立一個 Isolate,將一個回撥函式傳遞給新建立的 Isolate 以開始進行計算,從計算中返回一個值,並在計算完成後關閉 Isolate。這一切都與主 Isolate 並行發生,並且不會阻塞它。

Isolate.run 方法需要一個引數,即一個在新的 Isolate 上執行的回撥函式。該回調函式的函式簽名必須正好有一個必需的、無名稱的引數。當計算完成時,它會將回調的值返回給主 Isolate,並退出建立的 Isolate。
例如,考慮這段從檔案中載入大型 JSON 塊並將其轉換為自定義 Dart 物件的程式碼。如果 JSON 解碼過程沒有分載到新的 Isolate,此方法將導致 UI 在幾秒鐘內變得無響應。
// 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;
}有關使用 Isolate 在後臺解析 JSON 的完整演練,請參閱 此 cookbook 示例。
有狀態的、長生命週期的 Isolate
#短暫的 Isolate 很方便使用,但建立新 Isolate 和從一個 Isolate 複製物件到另一個 Isolate 會產生效能開銷。如果您使用 Isolate.run 重複執行相同的計算,那麼建立不會立即退出的 Isolate 可能會獲得更好的效能。
要做到這一點,您可以使用 Isolate.run 抽象的幾個較低級別的 Isolate 相關 API。
當您使用 Isolate.run 方法時,新 Isolate 在將單個訊息返回給主 Isolate 後會立即關閉。有時,您需要長生命週期的 Isolate,它們可以隨著時間的推移向彼此傳遞多條訊息。在 Dart 中,您可以使用 Isolate API 和 Ports 來實現這一點。這些長生命週期的 Isolate 通常被稱為後臺工作執行緒。
當您有一個在應用程式生命週期中需要重複執行的特定程序,或者有一個在一段時間內執行並需要向主 Isolate 返回多個值的程序時,長生命週期的 Isolate 會很有用。
或者,您可以使用 worker_manager 來管理長生命週期的 Isolate。
ReceivePort 和 SendPort
#使用兩個類(除了 Isolate)來設定 Isolate 之間的長生命週期通訊:ReceivePort 和 SendPort。這些 Port 是 Isolate 之間通訊的唯一方式。
Port 的行為類似於 Stream,其中 StreamController 或 Sink 在一個 Isolate 中建立,偵聽器在另一個 Isolate 中設定。在這種類比中,StreamController 被稱為 SendPort,您可以使用 send() 方法“新增”訊息。ReceivePort 是偵聽器,當這些偵聽器接收到新訊息時,它們會呼叫提供的回撥函式,並將訊息作為引數傳遞。
有關設定主 Isolate 和工作 Isolate 之間雙向通訊的深入說明,請遵循 Dart 文件 中的示例。
在 Isolate 中使用平臺外掛
#從 Flutter 3.7 開始,您可以在後臺 Isolate 中使用平臺外掛。這為將繁重的、平臺相關的計算分載到不會阻塞 UI 的 Isolate 提供了許多可能性。例如,假設您正在使用原生宿主 API(例如 Android 上的 Android API、iOS 上的 iOS API 等)加密資料。以前,將資料編組到宿主平臺可能會浪費 UI 執行緒時間,而現在可以在後臺 Isolate 中完成。
平臺通道 Isolate 使用 BackgroundIsolateBinaryMessenger API。以下程式碼片段顯示了在後臺 Isolate 中使用 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'));
}Isolate 的侷限性
#如果您是從支援多執行緒的語言轉到 Dart,那麼期望 Isolate 像執行緒一樣工作是合理的,但事實並非如此。Isolate 擁有自己的全域性欄位,並且只能透過訊息傳遞進行通訊,從而確保 Isolate 中的可變物件只能在單個 Isolate 中訪問。因此,Isolate 受限於其對自身記憶體的訪問。例如,如果您有一個名為 configuration 的全域性可變變數,它會在建立的 Isolate 中被複製為一個新的全域性欄位。如果您在建立的 Isolate 中修改該變數,它在主 Isolate 中將保持不變。即使您將 configuration 物件作為訊息傳遞給新的 Isolate,也是如此。Isolate 的功能就是這樣,在您考慮使用 Isolate 時,這一點很重要。
Web 平臺和 compute
#Dart Web 平臺(包括 Flutter Web)不支援 Isolate。如果您以 Web 為目標構建 Flutter 應用,可以使用 compute 方法來確保程式碼能夠編譯。compute() 方法在 Web 上在主執行緒上執行計算,但在移動裝置上建立新執行緒。在移動和桌面平臺上,await compute(fun, message) 等同於 await Isolate.run(() => fun(message))。
有關 Web 上併發的更多資訊,請檢視 dart.dev 上的 併發文件。
無法訪問 rootBundle 或 dart:ui 方法
#所有 UI 任務和 Flutter 本身都與主 Isolate 相關聯。因此,您無法在建立的 Isolate 中使用 rootBundle 訪問資源,也無法在建立的 Isolate 中執行任何 widget 或 UI 工作。
來自宿主平臺的有限外掛訊息傳送到 Flutter
#藉助後臺 Isolate 平臺通道,您可以在 Isolate 中使用平臺通道將訊息傳送到宿主平臺(例如 Android 或 iOS),並接收對這些訊息的響應。但是,您無法接收來自宿主平臺的未經請求的訊息。
例如,您無法在後臺 Isolate 中設定長生命週期的 Firestore 偵聽器,因為 Firestore 使用平臺通道將更新推送到 Flutter,這些更新是未經請求的。但是,您可以在後臺查詢 Firestore 以獲取響應。
更多資訊
#有關 Isolate 的更多資訊,請檢視以下資源:
- 如果您正在使用許多 Isolate,請考慮 Flutter 中的 IsolateNameServer 類,或者一個複製了該功能以供不使用 Flutter 的 Dart 應用程式使用的 pub 包。
- Dart 的 Isolate 是 Actor 模型 的實現。
- isolate_agents 是一個抽象了 Port 並使建立長生命週期 Isolate 更容易的包。
- 閱讀有關
BackgroundIsolateBinaryMessengerAPI 的 公告。