【Flutter基础】Dart中的并发Isolate
前言
说到 Flutter 中的异步,我想大家都不陌生。一般我们使用 Future
、async-await
来进行网络请求、文件读取等异步加载,但要提到 Isolate ,大家就未必能够说的明白了,今天我就带大家了解下 Dart 中的并发 Isolate。
一、Isolate的基本用法
1.1 Isolate的基本用法
对于 Isolate ,我们一般通过 Isolate.spawn()
来实现并发处理。
const String downloadLink = '下载链接';
final resultPort = ReceivePort();
await Isolate.spawn(readAndParseJson, [resultPort.sendPort, downloadLink]);
String fileContent = await resultPort.first as String;
print('展示文件内容: $fileContent');
Isolate.spawn()
内部传递一个 entryPoint
初始化函数,用于执行异步操作。这里我们定义 readAndParseJson
函数,通过设置 延迟2秒
来模拟文件读取。
Future<void> readAndParseJson(List<dynamic> args) async {
SendPort resultPort = args[0];
String fileLink = args[1];
print('获取下载链接: $fileLink');
String fileContent = '文件内容';
await Future.delayed(const Duration(seconds: 2));
Isolate.exit(resultPort, fileContent);
}
运行结果:
1.2 Isolate的异常处理
由于 Isolate 开启的是一块新的隔离区,完全和启动的 Isolate 独立,自然是无法通过 try-catch
进行捕获。
好在 Isolate 提供了异常通知的能力,我们依旧可以通过 ReceivePort 来接收 Isolate 产生的异常,代码如下所示:
const String downloadLink = '下载链接';
final resultPort = ReceivePort();
await Isolate.spawn(
readAndParseJsonWithErrorHandle,
[resultPort.sendPort, downloadLink],
onError: resultPort.sendPort,
onExit: resultPort.sendPort,
);
// 获取结果
final response = await resultPort.first;
if (response == null) { // 没有消息
print('没有消息');
} else if (response is List) { // 异常消息
final errorAsString = response[0]; //异常
final stackTraceAsString = response[1]; // 堆栈信息
print('error: $errorAsString \nstackTrace: $stackTraceAsString');
} else { // 正常消息
print(response);
}
在readAndParseJsonWithErrorHandle
函数中,我们通过手动 throw Exception
来触发异常处理。
Future<void> readAndParseJsonWithErrorHandle(List<dynamic> args) async {
SendPort resultPort = args[0];
String fileLink = args[1];
String newLink = '文件链接';
await Future.delayed(const Duration(seconds: 2));
throw Exception('下载失败');
Isolate.exit(resultPort, newLink);
}
运行结果:
1.3 Isolate.run()
如果我们每次使用时都需要通过 ReceivePort 来实现 Isolate 的消息通信,这样会过于繁琐。好在官方也考虑到了这个问题,通过提供 Isolate.run()
来直接获取返回值:
注意:该方法需要在 Dart 2.19 以上的版本使用,对应 Flutter 3.7.0 以上。
const String downloadLink = '下载链接';
String fileContent = await Isolate.run(() => handleReadAndParseJson(downloadLink));
print('展示文件内容: $fileContent');
/// 处理读取并解析文件内容
Future<String> handleReadAndParseJson(String fileLink) async {
print('获取下载链接: $fileLink');
String fileContent = '文件内容';
await Future.delayed(const Duration(seconds: 2));
return fileContent;
}
其原理就是内部通过对 Isolate.spawn()
进行封装,通过 Completer 来实现 Future 的异步回调。关键代码如下:
var result = Completer<R>();
var resultPort = RawReceivePort();
...
try {
Isolate.spawn(_RemoteRunner._remoteExecute,
_RemoteRunner<R>(computation, resultPort.sendPort),
onError: resultPort.sendPort,
onExit: resultPort.sendPort,
errorsAreFatal: true,
debugName: debugName)
.then<void>((_) {}, onError: (error, stack) {
// Sending the computation failed asynchronously.
// Do not expect a response, report the error asynchronously.
resultPort.close();
result.completeError(error, stack);
});
} on Object {
// Sending the computation failed synchronously.
// This is not expected to happen, but if it does,
// the synchronous error is respected and rethrown synchronously.
resultPort.close();
rethrow;
}
Tip:从官方的源码中我们可以学到,在调用
Isolate.spawn()
时,建议通过tray-catch
捕获可能发生的异常,并且在最后需要关闭 ReceivePort 避免内存泄漏。
二、Flutter中的compute
除了上述在 Dart 中的用法外,我们还可以在 Flutter 中通过 compute()
来实现。并且这也是官方推荐的用法,因为 compute()
允许在非原生平台 Web 上运行。
官方原文:If you’re using Flutter, consider using Flutter’s
compute()
function instead ofIsolate.run()
. Thecompute
function allows your code to work on both native and non-native platforms. UseIsolate.run()
when targeting native platforms only for a more ergonomic API.
2.1 compute的使用
和 Isolate.run()
的使用方式类似,通过传入 callback
函数让 Isolate 执行:
String content = await compute((link) async {
print('开始下载: $link');
await Future.delayed(const Duration(seconds: 2));
return '下载的内容';
}, '下载链接');
print('完成下载: $content');
运行结果:
Tip:在引入 Flutter 包之前,我们可以直接右键
run 'islate.dart' with Coverage
在Coverage 运行;在引入 Flutter 包之后,我们就需要在手机上运行,可以通过命令:open -a simulator
启动一个 iOS 模拟器运行。
2.2 compute的异常处理
查看 compute()
源码发现,内部使用的是 Isolate.run()
,而 Isolate.run()
内部是通过 Completer 来完成异步回调的。因此,我们直接通过 try-catch
即可捕获异常:
try {
await compute((link) async {
await Future.delayed(const Duration(seconds: 2));
throw Exception('下载失败');
}, '下载链接');
} catch (e) {
print('error: $e');
}
print('结束');
运行结果:
2.3 compute的源码分析
compute()
实际是对 isolates.compute
的实例化:
const ComputeImpl compute = isolates.compute;
而 Isolates 却是通过不同的平台来指定引入的类,这样也印证了为什么官方推荐在 Flutter 中使用 compute()
。
import '_isolates_io.dart'
if (dart.library.js_util) '_isolates_web.dart' as isolates;
在 _isolates_io.dart 中是通过 Isolate.run()
来实现:
Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, {String? debugLabel}) async {
debugLabel ??= kReleaseMode ? 'compute' : callback.toString();
return Isolate.run<R>(() {
return callback(message);
}, debugName: debugLabel);
}
在 _isolates_web.dart 中是通过 await null;
抽取单帧来执行函数:
Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
// To avoid blocking the UI immediately for an expensive function call, we
// pump a single frame to allow the framework to complete the current set
// of work.
await null;
return callback(message);
}
2.4 compute小结
- 因为
compute()
需要引入 flutter/foundation.dart,所以只能在 Flutter 中运行。 - 在 Flutter 中推荐使用
compute()
来实现,因为兼容 Web 平台。 - 其内部实现:在平台侧通过
Isolate.run()
,在 Web 侧通过await null;
抽取单帧来执行函数。
三、Isolate 的工作原理
在 Dart 中,Isolate 是一种类似于线程的概念,可以独立于其他 Isolate 运行,并且具有自己的堆栈和内存空间。这使得 Isolate 可以并行执行代码,并且不会受到其他 Isolate 的影响。
3.1 Future 为何还是会导致卡顿?
有时候我们可能会困惑,为什么明明已经使用了 Future
来异步执行任务,还是会出现卡顿的现象。那是因为 Dart 是单线程的,如果在执行 Future 时遇到耗时的计算任务或者 I/O操作,这些操作会占用当前线程的资源,从而导致应用出现卡顿现象,影响用户体验。
相比之下,Isolate 可以实现多线程并发执行任务,可以利用多核 CPU,因此可以更有效地处理大规模的计算密集型任务、I/O 密集型任务以及处理需要大量计算的算法等。在 Isolate 中执行任务不会占用 UI 线程的资源,从而可以保证应用的流畅性和响应速度。
3.2 Isolate 的工作原理
Isolate 的工作原理是通过使用 Dart 的隔离机制来实现的。每个 Isolate 都运行在独立的隔离环境中,并且与其他 Isolate 共享代码的副本。这意味着Isolate之间不能直接共享数据,而必须使用消息传递机制来进行通信。
其实我们在执行 main()
时,就开始了主 Isolate 的运行,如下图所示:
3.3 Isolate 的生命周期
Isolate的生命周期可以分为三个阶段:创建、运行和终止。
- 创建阶段:使用
Isolate.spawn()
方法可以创建一个新的 Isolate,并且将一个函数作为参数传递给这个方法。这个函数将作为新的 Isolate 的入口点,也就是 Isolate 启动时第一个执行的函数。创建 Isolate 时还可以指定其他参数,例如 Isolate 的名称、是否共享代码等等。 - 运行阶段:一旦创建了 Isolate,它就会开始执行入口点函数,并且进入事件循环。在事件循环中,Isolate 会不断地从消息队列中获取消息,并且根据消息的类型执行相应的代码。Isolate 可以同时执行多个任务,并且可以通过消息传递机制来协调这些任务的执行顺序。
- 终止阶段:当 Isolate 完成了它的任务,或者由于某些原因需要停止时,可以调用
Isolate.kill()
方法来终止 Isolate。此时,Isolate 会立即停止执行,并且 Isolate 对象和所有与它相关的资源都会被释放。
3.4 Isolate 组
在 Dart 2.15 也就是 Flutter 2.8 版本之后,当一个 Isolate 调用了 Isolate.spawn()
,两个 Isolate 将拥有同样的执行代码,并归入同一个 Isolate 组 中。Isolate 组会带来性能优化,例如新的 Isolate 会运行由 Isolate 组持有的代码,即共享代码调用。同时,Isolate.exit()
仅在对应的 Isolate 属于同一组时有效。
其原理是同一个 Isolate 组中的 Isolate 共享同一个堆,避免了对象的重复拷贝。这意味着生成一个新 Isolate 的速度提高了 100 倍,消耗的内存减少了 10-100 倍。
注意不要和前面的概念混淆,Isolate 仍然无法彼此共享内存,仍然需要消息传递。
四、使用场景
4.1 使用原则
- 如果一段代码不会被中断,那么就直接使用正常的同步执行就行。
- 如果代码段可以独立运行而不会影响应用程序的流畅性,建议使用 Future。
- 如果繁重的处理可能要花一些时间才能完成,而且会影响应用程序的流畅性,建议使用 Isolate。
4.2 耗时衡量
通过原则来判断可能过于抽象,我们可以用耗时来衡量:
- 对于耗时不超过
16ms
的操作推荐使用 Future。 - 对于耗时超过
16ms
以上的操作推荐使用 Isolate。
至于为什么用 16ms
作为衡量呢,因为屏幕一帧的刷新间隔就是 16ms
。
compute API文档原文:
/// {@template flutter.foundation.compute.usecase} /// This is useful for operations that take longer than a few milliseconds, and /// which would therefore risk skipping frames. For tasks that will only take a /// few milliseconds, consider [SchedulerBinding.scheduleTask] instead. /// {@endtemplate}
五、结语
至此,我们完成了对 Isolate 概念和用法的认识。项目源码:Fitem/flutter_article
如果觉得这篇文章对你有所帮助的话,不要忘了一键三连哦,大家的点赞是我更新的动力🥰。最后祝大家周末愉快~
参考资料:
链接:https://juejin.cn/post/7211539869805805623
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。