Skip to main content

并发和隔离区

所有 Dart 代码都在隔离区中运行,隔离区类似于线程,但不同之处在于隔离区拥有各自独立的内存。它们不会以任何方式共享状态,只能通过消息传递进行通信。默认情况下,Flutter 应用在其单个隔离区(主隔离区)上执行所有工作。在大多数情况下,此模型允许更简单的编程,并且速度足够快,不会导致应用程序的 UI 变得无响应。

但是,有时应用程序需要执行非常大的计算,这可能会导致“UI 卡顿”(运动不流畅)。如果您的应用因此出现卡顿,您可以将这些计算移到辅助隔离区。这允许底层运行时环境与主 UI 隔离区的工作同时运行计算,并利用多核设备的优势。

每个隔离区都有自己的内存和自己的事件循环。事件循环按事件添加到事件队列的顺序处理事件。在主隔离区上,这些事件可以是处理用户在 UI 中的点击、执行函数或在屏幕上绘制帧等任何内容。下图显示了一个带有 3 个等待处理的事件的事件队列示例。

主隔离区图

为了流畅渲染,Flutter 每秒(对于 60Hz 设备)向事件队列添加 60 次“绘制帧”事件。如果这些事件没有及时处理,应用程序就会出现 UI 卡顿,或者更糟的是完全无响应。

事件卡顿图

每当某个进程无法在一帧间隔(两帧之间的时间)内完成时,最好将工作卸载到另一个隔离区,以确保主隔离区可以每秒产生 60 帧。当您在 Dart 中生成隔离区时,它可以与主隔离区同时处理工作,而不会阻塞它。

您可以在 Dart 的并发页面上阅读有关隔离区和事件循环如何工作的更多信息。


Isolates and the event loop | Flutter in Focus

隔离区的常见用例

#

只有一个关于何时应该使用隔离区的硬性规定,那就是当大型计算导致您的 Flutter 应用程序出现 UI 卡顿时。当任何计算花费的时间超过 Flutter 的帧间隔时,就会发生这种卡顿。

事件卡顿图

任何进程都_可能_ 需要更长的时间才能完成,这取决于实现和输入数据,因此不可能创建关于何时需要考虑使用隔离区的详尽列表。

也就是说,隔离区通常用于以下情况:

  • 从本地数据库读取数据
  • 发送推送通知
  • 解析和解码大型数据文件
  • 处理或压缩照片、音频文件和视频文件
  • 转换音频和视频文件
  • 当您在使用 FFI 时需要异步支持
  • 将过滤应用于复杂的列表或文件系统

隔离区之间的消息传递

#

Dart 的隔离区是Actor 模型的一种实现。它们只能通过消息传递相互通信,这是使用Port 对象完成的。当消息在彼此之间“传递”时,它们通常从发送隔离区复制到接收隔离区。这意味着传递到隔离区的任何值,即使在该隔离区中被修改,也不会改变原始隔离区中的值。

唯一传递时不会被复制的对象到隔离区的是不可变对象,无论如何都不能更改,例如字符串或不可修改的字节。当您在隔离区之间传递不可变对象时,会通过端口发送对该对象的引用,而不是复制该对象,以提高性能。因为不可变对象不能更新,所以这有效地保留了 Actor 模型的行为。

此规则的一个例外是,当隔离区使用 Isolate.exit 方法发送消息时退出。因为发送隔离区在发送消息后将不复存在,所以它可以将消息的所有权从一个隔离区传递到另一个隔离区,确保只有一个隔离区可以访问该消息。

发送消息的两个最低级别的原语是 SendPort.send,它在发送时会复制可变消息,以及 Isolate.exit,它发送对消息的引用。Isolate.runcompute 在后台都使用 Isolate.exit

短生命周期隔离区

#

在 Flutter 中将进程移至隔离区的最简单方法是使用 Isolate.run 方法。此方法会生成一个隔离区,将回调传递到生成的隔离区以启动一些计算,从计算中返回值,然后在计算完成后关闭隔离区。所有这些都与主隔离区同时发生,并且不会阻塞它。

隔离区图

Isolate.run 方法需要一个参数,即在新的隔离区上运行的回调函数。此回调的函数签名必须恰好有一个必需的未命名参数。当计算完成后,它会将回调的值返回到主隔离区,并退出生成的隔离区。

例如,考虑这段代码,它从文件中加载大型 JSON 块,并将该 JSON 转换为自定义 Dart 对象。如果 JSON 解码过程没有卸载到新的隔离区,此方法会导致 UI 在几秒钟内变得无响应。

dart
// 生成包含 211,640 个照片对象的列表。
// (JSON 文件大小约为 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 和 Port 来实现此目的。这些长生命周期隔离区通常被称为 后台工作器

当您有一个需要在应用程序的整个生命周期中反复运行的特定进程,或者您有一个在一段时间内运行并需要向主隔离区产生多个返回值的进程时,长生命周期隔离区非常有用。

或者,您可以使用worker_manager 来管理长生命周期隔离区。

ReceivePort 和 SendPort

#

使用两个类(除了 Isolate 之外)来设置隔离区之间的长生命周期通信:ReceivePortSendPort。这些端口是隔离区唯一可以相互通信的方式。

Port 的行为类似于 Stream,其中 StreamControllerSink 在一个隔离区中创建,监听器在另一个隔离区中设置。在这个比喻中,StreamConroller 称为 SendPort,您可以使用 send() 方法“添加”消息。ReceivePort 是监听器,当这些监听器收到新消息时,它们会使用消息作为参数调用提供的回调。

有关设置主隔离区和工作隔离区之间双向通信的深入解释,请遵循Dart 文档中的示例。

在隔离区中使用平台插件

#

从 Flutter 3.7 开始,您可以在后台隔离区中使用平台插件。这为将繁重的、依赖于平台的计算卸载到不会阻塞 UI 的隔离区打开了多种可能性。例如,假设您正在使用本机主机 API(例如 Android 上的 Android API、iOS 上的 iOS API 等)加密数据。以前,将数据编组到主机平台可能会浪费 UI 线程时间,现在可以在后台隔离区中完成。

平台通道隔离区使用BackgroundIsolateBinaryMessenger API。以下代码片段显示了一个在后台隔离区中使用 shared_preferences 包的示例。

dart
import 'dart:isolate';

import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  // 确定要传递到后台隔离区的根隔离区。
  RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
  Isolate.spawn(_isolateMain, rootIsolateToken);
}

Future<void> _isolateMain(RootIsolateToken rootIsolateToken) async {
  // 将后台隔离区注册到根隔离区。
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);

  // 您现在可以使用 shared_preferences 插件。
  SharedPreferences sharedPreferences = await SharedPreferences.getInstance();

  print(sharedPreferences.getBool('isDebug'));
}

隔离区的局限性

#

如果您来自具有多线程的语言,那么期望隔离区像线程一样运行是合理的,但事实并非如此。隔离区有自己的全局字段,只能通过消息传递进行通信,确保隔离区中的可变对象仅在一个隔离区中可访问。因此,隔离区受到其对自身内存访问的限制。例如,如果您有一个应用程序,其中有一个名为 configuration 的全局可变变量,它会在生成的隔离区中被复制为新的全局字段。如果您在生成的隔离区中修改该变量,它在主隔离区中保持不变。即使您将 configuration 对象作为消息传递到新的隔离区,情况也是如此。这就是隔离区的预期运行方式,在您考虑使用隔离区时,务必记住这一点。

Web 平台和计算

#

Dart Web 平台,包括 Flutter Web, 不支持隔离区。如果您使用 Flutter 应用面向 Web,可以使用 compute 方法来确保您的代码可以编译。compute() 方法在 Web 上的主线程上运行计算,但在移动设备上会生成一个新线程。在移动和桌面平台上,await compute(fun, message) 等效于 await Isolate.run(() => fun(message))

有关 Web 上并发的更多信息,请查看 dart.dev 上的并发文档

无法访问 rootBundledart:ui 方法

#

所有 UI 任务和 Flutter 本身都与主隔离区耦合。因此,您无法在生成的隔离区中使用 rootBundle 访问资源,也无法在生成的隔离区中执行任何 widget 或 UI 工作。

来自主机平台到 Flutter 的插件消息有限

#

使用后台隔离区平台通道,您可以在隔离区中使用平台通道向主机平台(例如 Android 或 iOS)发送消息,并接收这些消息的响应。但是,您无法接收来自主机平台的主动消息。

例如,您无法在后台隔离区中设置长期存在的 Firestore 监听器,因为 Firestore 使用平台通道将更新推送到 Flutter,这些更新是主动的。但是,您可以查询 Firestore 以在后台获取响应。

更多信息

#

有关隔离区的更多信息,请查看以下资源:

  • 如果您正在使用许多隔离区,请考虑 Flutter 中的IsolateNameServer 类,或者克隆非 Flutter Dart 应用程序功能的 pub 包。
  • Dart 的隔离区是Actor 模型的一种实现。
  • isolate_agents 是一个抽象 Port 并使其更容易创建长生命周期隔离区的包。
  • 阅读有关 BackgroundIsolateBinaryMessenger API 公告 的更多信息。