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