并发与隔离区
使用 Dart 隔离在 Flutter 中进行多线程。
所有 Dart 代码都在 隔离 中运行,类似于线程,但不同之处在于隔离拥有自己的隔离内存。它们以任何方式都不共享状态,并且只能通过消息传递进行通信。默认情况下,Flutter 应用程序在单个隔离中完成所有工作——主隔离。在大多数情况下,这种模型允许更简单的编程并且足够快,以至于应用程序的 UI 不会变得无响应。
但是,有时应用程序需要执行异常大的计算,这可能会导致“UI 卡顿”(抖动)。如果您的应用程序由于此原因而遇到卡顿,您可以将这些计算移动到辅助隔离。这允许底层运行时环境与主 UI 隔离的工作同时运行计算,并利用多核设备。
每个隔离都有自己的内存和自己的事件循环。事件循环按事件添加到事件队列的顺序处理事件。在主隔离中,这些事件可以是处理 UI 中用户点击的任何内容,执行函数,或在屏幕上绘制帧。下图显示了一个事件队列示例,其中有 3 个等待处理的事件。
为了流畅的渲染,Flutter 会在每秒 60 次(对于 60Hz 设备)将“绘制帧”事件添加到事件队列中。如果这些事件未及时处理,应用程序将遇到 UI 卡顿,或者更糟,完全无响应。
每当一个过程无法在帧间隙(两个帧之间的时间)内完成时,将工作卸载到另一个隔离是一个好主意,以确保主隔离可以每秒生成 60 帧。当您在 Dart 中生成一个隔离时,它可以与主隔离同时处理工作,而不会阻塞它。
您可以在 Dart 文档的 并发页面 上了解有关隔离和事件循环如何工作的更多信息。
隔离的常见用例
#何时使用隔离只有一个硬性规则,那就是当大型计算导致您的 Flutter 应用程序遇到 UI 卡顿时。当任何计算时间超过 Flutter 的帧间隙时,就会发生此卡顿。
任何过程都可能需要更长的时间才能完成,具体取决于实现和输入数据,因此不可能创建一个详尽的列表,说明何时需要考虑使用隔离。
也就是说,隔离通常用于以下情况:
- 从本地数据库读取数据
- 发送推送通知
- 解析和解码大型数据文件
- 处理或压缩照片、音频文件和视频文件
- 转换音频和视频文件
- 在使用 FFI 时需要异步支持
- 对复杂列表或文件系统应用过滤
隔离之间的消息传递
#Dart 的隔离是 Actor 模型 的实现。它们只能通过消息传递进行通信,这通过 Port 对象 完成。当消息在彼此之间“传递”时,它们通常从发送隔离复制到接收隔离。这意味着传递到隔离的任何值,即使在该隔离中发生更改,也不会更改原始隔离中的值。
唯一 在传递到隔离时未复制的对象 是无论如何都无法更改的不可变对象,例如 String 或不可修改的字节。当您在隔离之间传递不可变对象时,会发送对该对象的引用,而不是复制该对象,以提高性能。由于不可变对象无法更新,因此实际上保留了 actor 模型行为。
此规则的一个例外是,当隔离使用 Isolate.exit 方法发送消息时,隔离退出。由于发送隔离在发送消息后将不再存在,因此它可以将消息的所有权从一个隔离传递到另一个隔离,确保只有一个隔离可以访问该消息。
发送消息的两个最低级别的原语是 SendPort.send,它在发送时复制可变消息,以及 Isolate.exit,它发送消息的引用。Isolate.run 和 compute 都在底层使用 Isolate.exit。
短生命周期隔离
#在 Flutter 中将过程移动到隔离的最简单方法是使用 Isolate.run 方法。此方法生成一个隔离,将回调传递给生成的隔离以启动一些计算,从计算返回一个值,然后在计算完成时关闭隔离。所有这些都与主隔离并发进行,并且不会阻塞它。
Isolate.run 方法需要一个参数,一个在新的隔离中运行的回调函数。此回调的函数签名必须具有一个必需的、未命名的参数。当计算完成时,它会将回调的值返回到主隔离,并退出生成的隔离。
例如,考虑这段代码,它从文件中加载一个大型 JSON blob,并将该 JSON 转换为自定义 Dart 对象。如果未将 json 解码过程卸载到新的隔离,此方法将导致 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;
}
有关使用隔离在后台解析 JSON 的完整演练,请参阅 此食谱。
有状态的、较长生命周期的隔离
#短生命周期隔离使用起来很方便,但生成新的隔离以及在隔离之间复制对象需要性能开销。如果您使用 Isolate.run 反复执行相同的计算,您可能会通过创建不立即退出的隔离来获得更好的性能。
为此,您可以使用一些 Isolate.run 抽象的较低级别的隔离相关 API:
当您使用 Isolate.run 方法时,新的隔离在将单个消息返回到主隔离后会立即关闭。有时,您需要长时间运行的隔离,并且可以随着时间的推移相互传递多个消息。在 Dart 中,您可以使用 Isolate API 和 Ports 来实现此目的。这些长时间运行的隔离通常被称为后台工作者。
长时间运行的隔离在您有一个需要在应用程序的整个生命周期中重复运行的特定过程时很有用,或者如果您有一个在一段时间内运行并需要向主隔离产生多个返回值的过程时很有用。
或者,您可以使用 worker_manager 来管理长时间运行的隔离。
ReceivePort 和 SendPort
#使用两个类(除了 Isolate)在隔离之间建立长时间的通信:ReceivePort 和 SendPort。这些端口是隔离通信的唯一方式。
Ports 的行为类似于 Streams,其中 StreamController 或 Sink 在一个隔离中创建,而侦听器在另一个隔离中设置。在这种类比中,StreamConroller 称为 SendPort,您可以使用 send() 方法“添加”消息。ReceivePort 是侦听器,当这些侦听器收到新消息时,它们会使用消息作为参数调用提供的回调。
有关设置主隔离和工作隔离之间双向通信的深入说明,请遵循 Dart 文档 中的示例。
在隔离中使用平台插件
#从 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'));
}
隔离的限制
#如果您来自具有多线程的语言,那么期望隔离的行为类似于线程是合理的,但事实并非如此。隔离拥有自己的全局字段,并且只能通过消息传递进行通信,确保隔离中的可变对象只能在单个隔离中访问。因此,隔离受到对其自身内存的访问的限制。例如,如果您有一个带有全局可变变量 configuration 的应用程序,它会作为新隔离中的新全局字段复制。如果您在生成的隔离中更改该变量,它在主隔离中将保持不变。即使您将 configuration 对象作为消息传递到新的隔离,也是如此。隔离的运作方式就是这样,在您考虑使用隔离时,请务必记住这一点。
Web 平台和计算
#Dart Web 平台,包括 Flutter Web,不支持隔离。如果您正在使用 Web 将 Flutter 应用程序作为目标,可以使用 compute 方法来确保您的代码编译。 compute() 方法在 Web 上主线程上运行计算,但在移动设备上生成一个新线程。在移动和桌面平台上,await compute(fun, message) 等效于 await Isolate.run(() => fun(message))。
有关 Web 上的并发的更多信息,请查看 dart.dev 上的并发文档。
无法访问 rootBundle 或 dart:ui 方法
#
所有 UI 任务和 Flutter 本身都与主隔离相关联。因此,您无法在生成的隔离中使用 rootBundle 访问资源,也无法在生成的隔离中执行任何小部件或 UI 工作。
从宿主平台到 Flutter 的有限插件消息
#使用后台隔离平台通道,您可以使用隔离中的平台通道将消息发送到主机平台(例如 Android 或 iOS),并接收对这些消息的响应。但是,您无法从主机平台接收未经请求的消息。
例如,你无法在后台 isolate 中设置一个持久的 Firestore 监听器,因为 Firestore 使用平台通道向 Flutter 推送更新,而这些更新是非请求的。但是,你可以在后台查询 Firestore 以获取响应。
更多信息
#有关 isolate 的更多信息,请查看以下资源:
- 如果你正在使用许多 isolate,请考虑 Flutter 中的 IsolateNameServer 类,或者用于非 Flutter 应用程序的 pub 包,该包复制了该功能。
- Dart 的 Isolates 是 Actor 模型 的一种实现。
- isolate_agents 是一个抽象了 Ports 并使创建持久 isolate 更加容易的包。
- 阅读有关
BackgroundIsolateBinaryMessengerAPI 的 公告。