并发和隔离区
所有 Dart 代码都在隔离区 (isolate)中运行,隔离区类似于线程,但不同之处在于隔离区拥有自己的独立内存。它们之间不会以任何方式共享状态,并且只能通过消息传递进行通信。默认情况下,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 和端口来实现这一点。这些长期存在的隔离区在口语中被称为后台工作进程。
当您有一个需要在应用程序的生命周期中重复运行的特定进程,或者当您有一个在一段时间内运行并需要向主隔离区产生多个返回值的进程时,长期存在的隔离区很有用。
或者,您可以使用worker_manager来管理长期存在的隔离区。
ReceivePorts 和 SendPorts
#使用两个类(除了 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'));
}
隔离区的局限性
#如果您来自使用多线程的语言,那么期望隔离区像线程一样工作是合理的,但事实并非如此。隔离区有自己的全局字段,并且只能通过消息传递进行通信,确保隔离区中的可变对象仅在单个隔离区中可访问。因此,隔离区在其对自身内存的访问方面受到限制。例如,如果您有一个应用程序,其中有一个名为 configuration
的全局可变变量,则它会在生成的隔离区中被复制为一个新的全局字段。如果您在生成的隔离区中修改该变量,则它在主隔离区中保持不变。即使您将 configuration
对象作为消息传递给新的隔离区,情况也是如此。这就是隔离区的工作方式,在您考虑使用隔离区时务必牢记这一点。
Web 平台和计算
#包括 Flutter Web 在内的 Dart Web 平台不支持隔离区。如果您使用 Flutter 应用程序定位 Web,则可以使用 compute
方法确保您的代码可以编译。compute()
方法在 Web 上的主线程上运行计算,但在移动设备上生成一个新线程。在移动和桌面平台上,await compute(fun, message)
等效于 await Isolate.run(() => fun(message))
。
有关 Web 上并发的更多信息,请查看 dart.dev 上的并发文档。
无法访问 rootBundle
或 dart:ui
方法
#所有 UI 任务和 Flutter 本身都与主隔离区耦合。因此,您不能在生成的隔离区中使用 rootBundle
访问资产,也不能在生成的隔离区中执行任何 widget 或 UI 工作。
主机平台到 Flutter 的插件消息有限
#使用后台隔离平台通道,您可以在隔离区中使用平台通道向主机平台(例如 Android 或 iOS)发送消息,并接收这些消息的回复。但是,您无法接收来自主机平台的主动消息。
例如,您无法在后台隔离区中设置长期存在的 Firestore 监听器,因为 Firestore 使用平台通道将更新推送到 Flutter,而这些更新是主动的。但是,您可以在后台查询 Firestore 以获取响应。
更多信息
#有关隔离区的更多信息,请查看以下资源
- 如果您正在使用许多隔离区,请考虑使用 Flutter 中的 IsolateNameServer 类,或克隆该功能的 pub 包,用于不使用 Flutter 的 Dart 应用程序。
- Dart 的隔离区是 Actor 模型 的实现。
- isolate_agents 是一个抽象了端口并使创建长期存在的隔离区变得更容易的包。
- 阅读有关
BackgroundIsolateBinaryMessenger
API 的更多信息 公告。
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面上次更新于 2024-11-19。 查看源代码 或 报告问题.