Flutter单线程模型之Isolate

170 阅读8分钟

1. 什么是Ioslate

我们的flutter应用启动的时候就会开辟一个独立的ioslate,这里面包含了一个独立的内存空间和一个携带 event loops的单一线程,这个单一线程只处理事件循环,我们所有dart代码都在ioslate里面执行,所有的事件,例如布局构建和拆除,异步任务,io事件等都是在这里面执行,每个事件都会被加入到一个事件队列中,由event loops从队列中按照先进先出的方式取出事件依次执行。(和android的handler机制挺像的嘛)

bb36b811ec7e776a008de2894645e2a4__fallback_source=1&height=1280&mount_node_token=BGoWdoOmEoYIEQxI1swc6CkxnJh&mount_point=docx_image&policy=equal&width=1280.png

如上图所示,就代表一个独立的ioslate 它就像我们机器上面开辟的一个小空间,绿色部分就代表一个独立的内存空间,红色部就代表事件循环,在这种方式下,我们是不能直接在dart中做大数据量的计算的,写flutter的开发者基本都是从android或ios转过来的,在android那边,对于大数据量的计算,为了不阻塞ui线程,我们可以使用开启异步任务执行大数据量的计算,但是在dart这边是不能这么做的,因为dart默认所有代码都在默认的ioslate中执行,而异步任务async对于ioslate来说也只是一个事件而已,而事件循环只有在线程空闲的时候才会从事件队列中取出事件执行,大数据量的计算导致线程不空闲,从而阻塞事件队列,导致应用掉帧,对于这个问题,我们可以使用Isolate.spawn()Flutter's compute()函数新建独立的ioslate执行大数据量的计算,让我们的 main isolate可以有空闲的时间来处理小部件的重建和销毁。

image.png

这些新建的isolate虽然由main isolate创建,但是main ioslate却不能直接访问child ioslate的内存,这就是ioslate的名字由来:这些小空间彼此隔离。

不同ioslate之间可以使用ReceivePort相互访问,他们之间唯一的工作方式就是通过不停的消息传递将事件传递给对方,在将事件加入到自己的事件队列中。

2. event loops

image.png

如图所示,如果把应用的生命周期比作一条时间线,那么在这条时间线中会发生各种事件,例如点击事件,widget的构建和销毁,io事件等,而这些事件会在任何时刻以任意的组合方式出现在事件队列中,event loops 会在空闲的时候从事件队列中按照先进先出的方式取出事件执行,一直到事件队列被清空为止,当事件队列被清空,并且所有的事件都被执行结束后,ioslate中的线程就会挂起,这个时候就可以执行gc操作,清除内存。

而异步编程就是建立在这样的基础上的,如下代码所示:

RaisedButton( //1. a event to build RaisedButton
  child: Text('Click me'),
  onPressed: () { //2. a event to send onTap 
    final myFuture = http.get('https://example.com'); //3. a event to send request 
    myFuture.then((response) { 
      if (response.statusCode == 200) {
        print('Success!');
      }
    });
  },
)

在代码中,发起了三个事件:

  1. 构建一个RaisedButton
  2. 用户发起的tap事件
  3. 注册一个异步回调,等待网络请求

不管是构建部件,还是异步任务,甚至异步任务的回调,对于 event loops来说,他们都只是一个事件而已,所以event loops它不管接下来出现的是什么事件,它只管从事件队列中取出事件执行,只要线程处于空闲下,它就不停的取出队列中的任务执行。上面的代码会向 event loops中塞入一个构建按钮的事件,执行完后等待用户点击事件的到来,当用户点击事件到来后会告诉flutter点击的坐标,flutter会根据坐标从渲染系统中找出对应的按钮的onPressed属性,执行相应的代码,这个时候,又会发出一个网络请求,并且注册回调,但是这对于 event loops来说也就是一个事件而已, event loops只管执行事件,当网络请求结束后会发起回调。

3. event loops处理的事件

  • I/O
  • gesture
  • drawing
  • timers
  • streams
  • futures

4. isolate(隔离区)的使用和通信

1. 创建隔离区

可以使用Isolate.spawn()Flutter's compute()函数新建新的隔离区,这个过程中,会新建新的线程和event loops,分配新的cpu资源和内存区域,代码如下

import 'dart:isolate';

void main() {
  Isolate.spawn(isolate, "true");
}

void isolate(String data) {
  print("isolate ${data}");
}

2. 主隔离区

flutter应用会创建一个默认的隔离区,可以使用下面的代码访问

Isolate.current

3. 隔离区之间的通信

使用ReceivePort实现不同隔离区的通信

import 'dart:isolate';

late Isolate isolate;

void sendMsg(SendPort sendPort) {
  sendPort.send('hellow wrold');
}

main() async {
  final receivePort = ReceivePort();
  isolate = await Isolate.spawn(sendMsg, receivePort.sendPort);
  receivePort.listen((message) {
    print(message); 
  });
}

4. 销毁隔离区

销毁隔离区的代码如下

isolate.kill();

5. MicroTask queue

image.png

如上图所示,我们的flutter应用在创建后不仅有 event quene,还存在着 microTask queue(微任务队列),而且在事件循环中,microTask queue事件处理的优先级其实是比event quene事件更高的,也就是说microTask queue中的事件会比event quene的事件更快被处理,event quene的事件必须等到microTask queue的事件被清空了才能被处理,所以说,一般情况下,我们是不会用到MicroTask queue的,但是一旦用到了,我们不能在microTask queue中做特别耗时的任务,否则会导致event quene的事件无法被处理,而widget的构建和刷新是event quene的事件,这样就会导致UI卡顿掉帧

6.最终思考与总结

1. 思考

用Future做网络请求不会导致卡顿,这是为什么

既然Future对于event loops来说只是一个事件,使用Future做耗时任务会导致UI卡顿,那为什么我们使用Future做网络请求(时长可能有十几秒)的时候不会导致掉帧或者卡顿呢?这是因为网络请求(http.get())就不是在dart层完成的,它其实是由dart层告诉操作系统,操作系统开启了一个异步任务完成的,操作系统做完网络请求后将数据再告诉dart层,dart层再完成一个简单的读操作,这就是为什么一个网络请求可以长达十几秒又不卡UI的原因,因为网络请求就不是dart层完成的

知识储备(external关键字)

external是flutter提供的一个关键字,可以在非抽象类中实现类似抽象方法的方法,将方法的声明和实现相分离, external的作用如下:

  1. 可以在非抽象类中实现类似抽象方法的方法,将方法的声明和实现相分离,这样可以实现在不同的平台,不管是dart for vm, 还是dart for web, 上层应用都可以只使用一套api,然后由不同的平台各种对external 修饰的方法添加实现,有助于提升扩展性,减低应用实现跨平台的难度

  2. external 声明的方法由底层sdk根据不同的平台(vm或者web)添加实现,方法所在类不用声明为抽象类,所以可以直接实例化

源码解析

我们查看Dio源码,发现使用Dio做网络请求,其实网络请求会交给HttpClient,由HttpClient的openUrl方法开启网络请求,而openUrl其实会调用到一个external方法,代码如下

image.png

我们在flutter3.3.2\flutter\bin\cache\dart-sdk\lib_internal\vm\socket_patch.dart找到了对应的实现,代码如下

image.png

我们继续查看这个_startConnect方法的调用过程,发现最终会调用到另一个external方法,代码如下

image.png

nativeCreateUnixDomainConnect也是一个external方法,我们暂时没有找到它的实现,但是我们其实可以知道Unix domain socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信,然后我们再看看nativeCreateUnixDomainConnect方法所在类的一些注释,会发现一些关键信息,代码如下

image.png

画了红圈是关键的一句注释,这句注释的意思就是说_NativeSocket封装了一个操作系统的socket,os是操作系统的意思,也就是说调用socket.nativeCreateUnixDomainConnect方法的时候会到调用操作系统的socket,也就是说网络请求其实是操作系统完成的,这就是为什么flutter应用是单线程模型的应用,但是在默认的isolate做网络请求却不会卡 UI 的原因,因为网络请求就不是dart层完成的,是操作系统完成的

2. 总结

我们了解了dart的单线程模型和事件机制,知道了widget的构建和刷新是在事件循环的条件下完成的,这使我们知道UI掉帧的本质原因(event queue 的UI刷新事件没有及时被处理),也知道了新建future和microTask 无法解决UI卡顿的问题,网络请求不会卡UI是因为网络请求就不是dart层完成的,是操作系统完成的,还知道了如何新建隔离区,这可以为我们日后做UI流畅度优化提供更多思路,带领我们走向正确的优化方向