併發與隔離區
使用 Dart Isolate 在 Flutter 中實現多執行緒。
所有 Dart 程式碼都在 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 中如何工作的資訊。
Isolate 的常見用例
#關於何時應該使用 Isolate,只有一個硬性規則:當繁重的計算導致 Flutter 應用出現 UI 卡頓,且計算時間超過了 Flutter 的幀間隙時,就應該使用它們。
任何程序都有可能需要更長的時間來完成,具體取決於實現方式和輸入資料,因此無法列出所有需要考慮使用 Isolate 的情況。
話雖如此,Isolate 通常用於以下場景:
- 從本地資料庫讀取資料
- 傳送推送通知
- 解析和解碼大型資料檔案
- 處理或壓縮照片、音訊檔案和影片檔案
- 轉換音訊和影片檔案
- 使用 FFI 時需要非同步支援的情況
- 對複雜列表或檔案系統應用過濾
Isolate 之間的訊息傳遞
#Dart 的 Isolate 是 Actor 模型的一種實現。它們只能透過訊息傳遞進行相互通訊,這是透過 Port 物件完成的。當訊息在它們之間“傳遞”時,通常會從傳送方 Isolate 複製到接收方 Isolate。這意味著傳遞給 Isolate 的任何值,即使在該 Isolate 中被修改,也不會改變原始 Isolate 中的值。
當傳遞給 Isolate 時,唯一 不會被複制的物件是本來就不可變的物件,例如 String 或不可修改的位元組流。當你向 Isolate 傳遞不可變物件時,為了獲得更好的效能,會透過埠傳送該物件的引用,而不是複製物件。由於不可變物件無法更新,這有效地保持了 Actor 模型的行為。
此規則的一個例外是當 Isolate 使用 Isolate.exit 方法傳送訊息並退出時。由於傳送方 Isolate 在傳送訊息後將不復存在,它可以將訊息的所有權從一個 Isolate 轉移到另一個,從而確保只有一個 Isolate 能訪問該訊息。
傳送訊息的兩個最底層原語是 SendPort.send(在傳送時複製可變訊息)和 Isolate.exit(傳送對訊息的引用)。Isolate.run 和 compute 在底層都使用了 Isolate.exit。
短生命週期 Isolate
#在 Flutter 中將程序轉移到 Isolate 的最簡單方法是使用 Isolate.run 方法。該方法會生成一個 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.run 執行相同的計算,透過建立不立即退出的 Isolate 可能會獲得更好的效能。
為此,你可以使用 Isolate.run 所抽象化的一些更底層的 Isolate 相關 API:
-
Isolate.spawn()和Isolate.exit() -
ReceivePort和SendPort send()方法
當你使用 Isolate.run 方法時,新 Isolate 在向主 Isolate 返回單個訊息後會立即關閉。有時,你需要長生命週期的 Isolate,以便它們能夠隨著時間的推移相互傳遞多條訊息。在 Dart 中,你可以使用 Isolate API 和 Ports 來實現這一點。這些長生命週期的 Isolate 通常被稱為後臺工作執行緒(background workers)。
當某個特定程序需要在應用的整個生命週期內重複執行,或者當某個程序需要執行一段時間並需要向主 Isolate 返回多個值時,長生命週期的 Isolate 非常有用。
或者,你也可以使用 worker_manager 來管理長生命週期的 Isolate。
ReceivePort 和 SendPort
#使用兩個類(除了 Isolate 之外)來設定 Isolate 之間的長生命週期通訊:ReceivePort 和 SendPort。這些埠是 Isolate 相互通訊的唯一途徑。
Ports 的行為類似於 Streams,其中 StreamController 或 Sink 在一個 Isolate 中建立,而監聽器在另一個 Isolate 中設定。在這個類比中,StreamController 稱為 SendPort,你可以使用 send() 方法“新增”訊息。ReceivePort 是監聽器,當這些監聽器收到新訊息時,它們會呼叫提供的回撥,並將訊息作為引數。
關於設定主 Isolate 與工作執行緒 Isolate 之間雙向通訊的深入解釋,請參考 Dart 文件中的示例。
在 Isolate 中使用平臺外掛
#你可以在後臺 Isolate 中使用平臺外掛。這使得外掛能夠將繁重的、依賴於平臺的計算解除安裝到不會阻塞 UI 的 Isolate 中。例如,假設你正在使用原生宿主 API(如 Android 上的 Android API,iOS 上的 iOS API 等)加密資料。以前,將資料 序列化(marshaling) 到宿主平臺可能會浪費 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 是一個抽象了 Ports 並簡化了長生命週期 Isolate 建立過程的包。
- 閱讀有關
BackgroundIsolateBinaryMessengerAPI 的更多資訊,請檢視 官方公告。