并发与隔离区
所有 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 更容易的包。
- 阅读有关
BackgroundIsolateBinaryMessenger
API 的 公告。