isolate和线程类似,但是每个isolate有他们自己的内存,他们不分享状态,只通过message互通消息。每一个isolate里都有一个事件循环,这个概念和JavaScript的线程概念多少有些相似。也就是说,如果在主isolate之外开了另外的一个或者多个isolate,那么他们之间可以通过message(消息)互相通信。
所有的Flutter app都在isolate上面运行。一般是只有一个,叫做main isolate。而且运行的也足够快,不会产生不良的影响。但是,难免出现需要执行大量的任务,从而导致界面出现了卡顿。在ioslate里,任何任务的事件只有最多16ms,这样才能保证界面60帧的刷新率。一旦代码的执行事件超过了这个限制就会出现界面的卡顿。 如果你正经历这个问题,那么可以把这些任务放在另外一个isolate里面。
代码执行事件和帧率的关系:
要怎么运行起来一个isolate呢?
Isolate.run
,一杆子买卖,一次性在另外的一个isolate上执行代码。也可以使用compute
这个方法。Isolate.spawn
, 这样创建的isolate会在后台长期存在。
Isolate.run
多数情况下第一种方式就可以解决了,比如计算一个斐波那契数列:
dart
int slowFib(int n) => n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
void fib40() async {
var result = await Isolate.run(() => slowFib(40));
print('Fib(40) = $result');
}
使用起来也是非常的简单。只需要在Isolate.run
前面放一个await
就可以得到结果了。
compute
前文说到,Isolate.run
和compute
基本类似。compute
是在Flutter的专属,相当于语法糖的存在。上例就可以写为:
dart
// final result = await Isolate.run(() => slowFib(target));
final result = await compute(slowFib, target);
return result;
算是一个小小的改进。
有一点需要注意的是:Flutter web不支持Isolate。
在web运行,卡了
Isolate.spawn
入口函数的定义
入口函数接收一个参数SendPort
, 用来向其他的isolate发送消息,一般来说就是那个spawn了isolate的isolate。
dart
Future<void> spawnFib(SendPort sendPort) async {
final result = slowFib(target);
sendPort.send(result); // 发送计算结果
}
现在你可能就有疑问了,要计算斐波那契数列,那不得知道target
的值是多少么。函数接收的参数只能发送计算的结果,没办法接收发送过来的target
的值。
这就需要先用SendPort
参数先把本isolate可以接收数据的SendPort
发到信息发送方就可以了。如:
dart
Future<void> spawnFib(SendPort sendPort) async {
final commandPort = ReceivePort();
sendPort.send(commandPort.sendPort);
// 在这里接收传入的信息,并验证是不是传入的target值
await for (final message in commandPort) {
if (message is int) {
// 略
}
// 略
}
}
Spawn一个isolate
上文讲了如何定义一个isolate的入口函数,下面来研究一下如何spawn一个isolate。
在上例中看到,要发送消息就要首先初始化一个接收消息的ReceivePort
。要开启一个isolate就是这样:
dart
// 先定义一个接收端
final p = ReceivePort();
// Spawn一个isolate,把接收端的发送端作为参数发送过去
await Isolate.spawn(spawnFib, p.sendPort);
- 第一步:初始化一个
ReceivePort
。 - 第二步:
spawn
一个isolate,同时把定义好的入口函数和ReceivePort
的SendPort
实例作为参数传入。
学会了以上的内容就可以定义一个isolate入口函数和spawn
一个isolate了。并且可以在两个isolate直接实现双向通信了。下面还有几个细节需要注意,既然可以互相发送消息,这些消息里内容的不同需要做一些区分。比如上文已经提到的isolate的入口函数,现在已经知道要发送两种不同类型的数据:
- 发送的是斐波那契数列的
target
。 - 发送的是供主isolate发送消息的
SendPort
。
这需要在spawn了isolate的主isolate里作区分。比如:
dart
// 略
await for (var response in p) {
if (response == null) {
break;
}
if (response is SendPort) {
sendPort = response;
sendPort.send(40);
break;
}
// TODO: show calulation result
debugPrint('received message $response');
}
// 略
同时,在主isolate里还有一件必须处理的事,那就是在任务完成之后需要终止子isolate的执行。这也要通过发送消息的方式通知子isolate。
在主isolate里发送通知:
dart
if (sendPort != null) sendPort.send(null);
在子isolate里执行isolate的退出:
dart
Future<void> spawnFib(SendPort sendPort) async {
final commandPort = ReceivePort();
sendPort.send(commandPort.sendPort);
await for (final message in commandPort) {
if (message is int) {
// 略
} else if (message == null) { // 收到主isolate发来的null消息
break;
}
}
debugPrint("Spawn isolate existing...");
Isolate.exit(); // 子isolate总之执行
}
主isolate里发送了null消息之后,在子isolate里:
- 检查消息是否是null的,如果是则退出await-for循环。
- 执行本isolate的退出操作。
完整的示例代码如下:
isolate入口函数
dart
Future<void> spawnFib(SendPort sendPort) async {
final commandPort = ReceivePort();
sendPort.send(commandPort.sendPort);
await for (final message in commandPort) {
if (message is int) {
// final target = int.parse(message);
final target = message;
debugPrint("received message $target");
final result = slowFib(target);
debugPrint("cal result $result");
sendPort.send(result);
} else if (message == null) {
break;
}
}
debugPrint("Spawn isolate existing...");
Isolate.exit();
}
在按钮点击时间里spawn一个上面的isolate:
dart
onPressed: () async {
final p = ReceivePort();
await Isolate.spawn(spawnFib, p.sendPort);
SendPort? sendPort;
await for (var response in p) {
if (response is SendPort) {
sendPort = response;
sendPort.send(40);
}
if (response is int) {
// TODO: show calulation result
debugPrint('received message $response');
break;
}
}
if (sendPort != null) sendPort.send(null);
},
最后
以上的例子只是实现了比较简单的功能。有兴趣的同学可以实现一个可以发送进度消息的,可以取消的isolate试试。
直接使用isolate来实现上面说的进度、取消的功能有点略微的繁琐。因此就可以考虑第三方库worker_manager
来实现这些功能。它提供了更多的封装和更丰富的功能,使用冯家方便。