Dart 2.15 更新后 isolate 应该这么用
序言
2021年的最后一天, Dart 官方发布了 dart 2.15 版本,该版本优化了很多内容,今天我们要重点说说 isolate 工作器。官方推文链接
在探索新变化之前,我们来回忆巩固一下 isolate 的使用。
isolate 的作用
问题:Flutter 基于单线程模式使用协程进行开发,为什么还需要 isolate ?
首先我们要明确 并行(isolate) 与并发(future)的区别。下面我们通过简单的例子来进行说明 。Demo 是一个简单的页面,中间放置一个不断转圈的 progress 和一个按键,按键用来触发耗时方法。
///计算偶数个数(具体的耗时操作)下面示例代码中会用到
static int calculateEvenCount(int num) {
int count = 0;
while (num > 0) {
if (num % 2 == 0) {
count++;
}
num--;
}
return count;
}
///按键点击事件
onPressed: () {
//触发耗时操作
doMockTimeConsume();
}
- 方式一: 我们将耗时操作使用
future的方式进行封装
///使用future的方式封装耗时操作
static Future<int> futureCountEven(int num) async {
var result = calculateEvenCount(num);
return Future.value(result);
}
///耗时事件
void doMockTimeConsume() async {
var result = await futureCountEven(1000000000);
_count = result;
setState(() {});
}
结果如下:
结论:使用 future 的方式来消费耗时操作,由于仍然是单线程在进行工作,异步只是在同一个线程的并发操作,仍会阻塞UI的刷新。
- 方式二: 使用
isolate创建新线程,避开主线程,不干扰UI刷新
//模拟耗时操作
void doMockTimeConsume() async {
var result = await isolateCountEven(1000000000);
_count = result;
setState(() {});
}
///使用isolate的方式封装耗时操作
static Future<dynamic> isolateCountEven(int num) async {
final p = ReceivePort();
///发送参数
await Isolate.spawn(_entryPoint, [p.sendPort, num]);
return (await p.first) as int;
}
static void _entryPoint(SendPort port) {
SendPort responsePort = args[0];
int num = args[1];
///接收参数,进行耗时操作后返回数据
responsePort.send(calculateEvenCount(num));
}
结果如下:
结论:使用 isolate 实现了多线程并行,在新线程中进行耗时操作不会干扰UI线程的刷新。
isolate 的局限性,为什么需要优化?
iso 有两点较为重要的局限性。
- isolate 消耗较重,除了创建耗时,每次创建还至少需要2Mb的空间,有OOM的风险。
- isolate 之间的内存空间各自独立,当参数或结果跨 iso 相互传递时需要深度拷贝,拷贝耗时,可能造成UI卡顿。
isolate 新特性
Dart 2.15 更新, 给 iso 添加了组的概念,isolate 组 工作特征可简单总结为以下两点:
- Isolate 组中的 isolate 共享各种内部数据结构
- Isolate 组
仍然阻止在 isolate 间共享访问可变对象,但由于 isolate 组使用共享堆实现,这也让其拥有了更多的功能。
官方推文中举了一个例子:
工作器 isolate 通过网络调用获得数据,将该数据解析为大型 JSON 对象图,然后将这个 JSON 图返回到主 isolate 中。
Dart 2.15 之前:执行该操作需要深度复制,如果复制花费的时间超过帧预算时间,就会导致界面卡顿。
使用 Dart 2.15:工作器 isolate 可以调用Isolate.exit(),将其结果作为参数传递。然后,Dart 运行时将包含结果的内存数据从工作器 isolate 传递到主 isolate 中,无需复制,且主 isolate 可以在固定时间内接收结果。
重点:提供 Isolate.exit() 方法,将包含结果的内存数据从工作器 isolate 传递到主 isolate ,过程无需复制。
附注: 使用 Dart 新特性,需将 flutter sdk 升级到 2.8.0 以上 链接。
isolate中:exit 和 send 的区别及用法
Dart 更新后,我们将数据从 工作器 isolate(子线程)回传到 主 isolate(主线程)有两种方式。
- 方式一: 使用
send
responsePort.send(data);
点击进入 send 方法查看源码注释,看到这样一句话:

结论:send 本身不会阻塞,会立即发送,但可能需要线性时间成本用于复制数据。
- 方式二:使用
exit
Isolate.exit(responsePort, data);
官网 给出的解释如下:

结论:隔离之间的消息传递通常涉及数据复制,因此可能会很慢,并且会随着消息大小的增加而增加。但是
exit(),则是在退出隔离中保存消息的内存,不会被复制,而是被传输到主 isolate。这种传输很快,并且在恒定的时间内完成。
我们把上面 demo 中的 _entryPoint 方法做一下优化,修改如下:
static void _entryPoint(SendPort port) {
SendPort responsePort = args[0];
int num = args[1];
///接收参数,进行耗时操作后返回数据
//responsePort.send(calculateEvenCount(num));
Isolate.exit(responsePort, calculateEvenCount(num));
}
总结:使用 exit() 替代 SendPort.send,可规避数据复制,节省耗时。
isolate 组
如何创建一个 isolate 组?官方给出的解释如下:
When an isolate calls
Isolate.spawn(), the two isolates have the same executable code and are in the same isolate group. Isolate groups enable performance optimizations such as sharing code; a new isolate immediately runs the code owned by the isolate group. Also,Isolate.exit()works only when the isolates are in the same isolate group.
当在 isolate 中调用另一个 isolate 时,这两个 isolate 具有相同的可执行代码,并且位于同一隔离组。
PS: 小轰暂时也没有想到具体的使用场景,先暂放一边吧。
实践:isolate 如何处理连续数据
结合上面的耗时方法calculateEvenCount,isolate 处理连续数据需要结合 stream 流 的设计。具体 demo 如下:
///测试入口
static testContinuityIso() async {
final numbs = [10000, 20000, 30000, 40000];
await for (final data in _sendAndReceive(numbs)) {
log(data.toString());
}
}
///具体的iso实现(主线程)
static Stream<Map<String, dynamic>> _sendAndReceive(List<int> numbs) async* {
final p = ReceivePort();
await Isolate.spawn(_entry, p.sendPort);
final events = StreamQueue<dynamic>(p);
// 拿到 子isolate传递过来的 SendPort 用于发送数据
SendPort sendPort = await events.next;
for (var num in numbs) {
//发送一条数据,等待一条数据结果,往复循环
sendPort.send(num);
Map<String, dynamic> message = await events.next;
//每次的结果通过stream流外露
yield message;
}
//发送 null 作为结束标识符
sendPort.send(null);
await events.cancel();
}
///具体的iso实现(子线程)
static Future<void> _entry(SendPort p) async {
final commandPort = ReceivePort();
//发送一个 sendPort 给主iso ,用于 主iso 发送参数给 子iso
p.send(commandPort.sendPort);
await for (final message in commandPort) {
if (message is int) {
final data = calculateEvenCount(message);
p.send(data);
} else if (message == null) {
break;
}
}
}
抛砖引玉,这只是一个思路~
转载自:https://juejin.cn/post/7049990302739726366