likes
comments
collection
share

Flutter开发-- Isolate

作者站长头像
站长
· 阅读数 9

isolate和线程类似,但是每个isolate有他们自己的内存,他们不分享状态,只通过message互通消息。每一个isolate里都有一个事件循环,这个概念和JavaScript的线程概念多少有些相似。也就是说,如果在主isolate之外开了另外的一个或者多个isolate,那么他们之间可以通过message(消息)互相通信。

所有的Flutter app都在isolate上面运行。一般是只有一个,叫做main isolate。而且运行的也足够快,不会产生不良的影响。但是,难免出现需要执行大量的任务,从而导致界面出现了卡顿。在ioslate里,任何任务的事件只有最多16ms,这样才能保证界面60帧的刷新率。一旦代码的执行事件超过了这个限制就会出现界面的卡顿。 如果你正经历这个问题,那么可以把这些任务放在另外一个isolate里面。 代码执行事件和帧率的关系:Flutter开发-- Isolate 要怎么运行起来一个isolate呢?

  • Isolate.run,一杆子买卖,一次性在另外的一个isolate上执行代码。也可以使用compute这个方法。
  • Isolate.spawn, 这样创建的isolate会在后台长期存在。

Isolate.run

多数情况下第一种方式就可以解决了,比如计算一个斐波那契数列:

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.runcompute基本类似。compute是在Flutter的专属,相当于语法糖的存在。上例就可以写为:

// final result = await Isolate.run(() => slowFib(target));
final result = await compute(slowFib, target);
return result;

算是一个小小的改进。

有一点需要注意的是:Flutter web不支持Isolate

在web运行,卡了

Flutter开发-- Isolate

Isolate.spawn

入口函数的定义

入口函数接收一个参数SendPort, 用来向其他的isolate发送消息,一般来说就是那个spawn了isolate的isolate。

Future<void> spawnFib(SendPort sendPort) async {
  final result = slowFib(target);

  sendPort.send(result); // 发送计算结果
}

现在你可能就有疑问了,要计算斐波那契数列,那不得知道target的值是多少么。函数接收的参数只能发送计算的结果,没办法接收发送过来的target的值。

这就需要先用SendPort参数先把本isolate可以接收数据的SendPort发到信息发送方就可以了。如:

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就是这样:

 // 先定义一个接收端
 final p = ReceivePort(); 
 
 // Spawn一个isolate,把接收端的发送端作为参数发送过去
 await Isolate.spawn(spawnFib, p.sendPort); 
  • 第一步:初始化一个ReceivePort
  • 第二步:spawn一个isolate,同时把定义好的入口函数和ReceivePortSendPort实例作为参数传入。

学会了以上的内容就可以定义一个isolate入口函数和spawn一个isolate了。并且可以在两个isolate直接实现双向通信了。下面还有几个细节需要注意,既然可以互相发送消息,这些消息里内容的不同需要做一些区分。比如上文已经提到的isolate的入口函数,现在已经知道要发送两种不同类型的数据:

  1. 发送的是斐波那契数列的target
  2. 发送的是供主isolate发送消息的SendPort

这需要在spawn了isolate的主isolate里作区分。比如:

// 略
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里发送通知:

if (sendPort != null) sendPort.send(null);

在子isolate里执行isolate的退出:

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里:

  1. 检查消息是否是null的,如果是则退出await-for循环。
  2. 执行本isolate的退出操作。

完整的示例代码如下:

isolate入口函数

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:

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来实现这些功能。它提供了更多的封装和更丰富的功能,使用冯家方便。