序言
2021年的最后一天, Dart 官方发布了 dart 2.15 版本,该版本优化了很多内容,今天我们要重点说说 isolate 工作器。官方推文链接
在探索新变化之前,我们来回忆巩固一下 isolate 的使用。
isolate 的作用
问题:Flutter 基于单线程模式使用协程进行开发,为什么还需要 isolate ?
首先我们要明确 并行(isolate) 与并发(future)的区别。下面我们通过简单的例子来进行说明 。Demo 是一个简单的页面,中间放置一个不断转圈的 progress 和一个按键,按键用来触发耗时方法。
|
1 2 3 4 5 6 7 8 9 10 11 |
///计算偶数个数(具体的耗时操作)下面示例代码中会用到 static int calculateEvenCount(int num) { int count = 0; while (num > 0) { if (num % 2 == 0) { count++; } num--; } return count; } |
|
1 2 3 4 5 |
///按键点击事件 onPressed: () { //触发耗时操作 doMockTimeConsume(); } |
- 方式一: 我们将耗时操作使用
future的方式进行封装
|
1 2 3 4 5 6 7 8 9 10 11 12 |
///使用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刷新
|
1 2 3 4 5 6 |
//模拟耗时操作 void doMockTimeConsume() async { var result = await isolateCountEven(1000000000); _count = result; setState(() {}); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
///使用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
|
1 |
responsePort.send(data); |
点击进入 send 方法查看源码注释,看到这样一句话:

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

结论:隔离之间的消息传递通常涉及数据复制,因此可能会很慢,并且会随着消息大小的增加而增加。但是
exit(),则是在退出隔离中保存消息的内存,不会被复制,而是被传输到主 isolate。这种传输很快,并且在恒定的时间内完成。
我们把上面 demo 中的 _entryPoint 方法做一下优化,修改如下:
|
1 2 3 4 5 6 7 |
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 如下:
|
1 2 3 4 5 6 7 |
///测试入口 static testContinuityIso() async { final numbs = [10000, 20000, 30000, 40000]; await for (final data in _sendAndReceive(numbs)) { log(data.toString()); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
///具体的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(); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
///具体的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; } } } |