flutter之并发Isolate

474 阅读4分钟

前提

  • 众所周知,dart是单线程语言,通过事件循环EventLoop同时处理多个事件。但当单个事件会阻塞线程时,往往使用异步处理Future,但Future能力是有限的,若单个事件处理时间过长还是会影响整个线程运行导致卡顿(EventLoop事件循环),此时就需要isolate来处理。
  • 为什么使用Future异步处理仍然会出现卡顿呢,因为 Dart 是单线程的,如果在执行 Future 时遇到耗时的计算任务或者 I/O操作,这些操作会占用当前线程的资源,从而导致应用出现卡顿现象,影响用户体验。
  • isolate顾名思义 隔离,每个isolate有自己的堆内存,isolate之间是隔离的,不共享内存,只能通过消息机制共享数据。通常情况下,单个drat运行main函数时,都会自动生成一个主isolate,接着执行事件代码。
  • 由此可知,当一个事件处理时间过长时,开启一个新的isolate并放该事件进去处理,最后返回给主事件,这样主事件就不会出现阻塞卡顿情况。因此,isolate又称为后台运行对象,即在后台运行事件,不会影响主线程。

isolate使用以及原理

ReceivePort

  1. 先讲isolate之间消息传播的原理ReceivePort(),生成一个实例var receive\color{green}{receive} = ReceivePort(),receive\color{green}{receive}里有个sendPort(),可以发送信息,发送到哪呢?
  2. 肯定是发送到自己的实例啦,即用receive\color{green}{receive}.listen((){})就能监听到sendPort发送的数据。
  3. 由以上可得,在开启新的isolate前要先在isolate\color{red}{主isolate}实例化一个ReceivePort,然后把该对象的sendPort传进去,用来发送数据出来。

Isolate.spawn

  • Isolate.spawn((message)=>dynamic,[args])
  • Isolate.spawn用于生成新的isolate,并将事件代码传进去运行。第一个参数为传进去的函数(取名为func\color{red}{func}),第二个参数【args】作为第一个参数的参数,即func\color{red}{func}的参数,主要用于传sendPort和要处理的数据,其他参数例如onError、onExit需要传主isolate的sendPort,目的是捕捉异常。
  • 具体例子如下:
import 'dart:isolate';


void main() {

  SendPort childSendPort;
  //创建新的具有发送器的isolate,第一个参数是具有内存隔离的新的isolate的具体业务逻辑函数,第二个是创建的isolate的时候传递的参数,一般我们传递当前isolate的发送器
  ReceivePort receivePort = new ReceivePort();
  Isolate.spawn(isolateVice, receivePort.sendPort);

  //主进程接受持有主进程发送器的isolate发过来的消息
  receivePort.listen((message) {
    //其他的isolate可以选择发过来自身的sendPort给主进程,则主进程isolate也可以向创建的isolate发送消息,完成交互操作
    if (message is SendPort) {
      childSendPort = message;
      message.send("———————— 已收到函数isolateVice()的发送器 ————————");
    } else {
      print("接到函数isolateVice()消息:" + message);
      if (childSendPort != null) {
        childSendPort.send('您喜欢我哪些,我改还不成嘛!'); //进行一次回复
      }
    }
  });
}

/// 内存隔离的新的isolate的具体业务逻辑函数
void isolateVice(SendPort sendPort) {

  ReceivePort receivePort = new ReceivePort();   //当前isolate的消息接收器
  //创建当前函数(isolateVice)的时候传递的第二个参数(这里我们认为是该iso的发送器),使用主iso的发送器将自身子iso的发送器发送过去,完成交互
  sendPort.send(receivePort.sendPort);

  sendPort.send("我喜欢你"); // 测试向主isolate发送消息
  receivePort.listen((message) {
    print("接到函数main()消息::" + message);
  });
}

Isolate.run()

  • 由于使用ReceivePort太过繁杂,因此dart封装了Isolate.run()函数,其内部自己生成ReceivePort并用Isolate.spawn()传入,简化了开发者的使用。
  • 注意:该方法需要在 Dart 2.19 以上的版本使用,对应 Flutter 3.7.0 以上。
  • 具体使用如下:
void main() async {
  // Read some data.
  final jsonData = await Isolate.run(() async {
    final fileData = await File(filename).readAsString();
    final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
    return jsonData;
  });

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

Isolate.compute()

  • 除了上述在 Dart 中的用法外,我们还可以在 Flutter 中通过 compute() 来实现。并且这也是官方推荐的用法,因为 compute() 允许在非原生平台 Web 上运行。

使用场景以及缺点

  • 一般情况下,常用的还是Future处理就好,但当时间处理超过16ms以上,就可以使用isolate.

PS:屏幕一帧的刷新间隔就是 16ms

常用场景

  1. JSON解析: 解码JSON,这是HttpRequest的结果,可能需要一些时间
  2. 加解密: 加解密过程比较耗时
  3. 图片处理: 比如裁剪图片比较耗时
  4. 从网络中加载大图

缺点:

  1. isolate 消耗较重,除了创建耗时,每次创建还至少需要2Mb的空间,有OOM的风险。
  2. isolate 之间的内存空间各自独立,当参数或结果跨 iso 相互传递时需要深度拷贝,拷贝耗时,可能造成UI卡顿。

OOM:out of memory,内存泄漏和内存溢出

dart2.5新特性,性能提升(能有效解决缺点2)

当一个 isolate 调用了 Isolate.spawn(),两个 isolate 将拥有同样的执行代码,并归入同一个 isolate 组 中。 Isolate 组会带来性能优化,例如新的 isolate 会运行由 isolate 组持有的代码,即共享代码调用。同时,Isolate.exit() 仅在对应的 isolate 属于同一组时有效。

总结:使用 exit() 替代 SendPort.send,可规避数据复制,节省耗时。

参考资料:

Isolate 的工作原理

【Flutter基础】Dart中的并发Isolate