Flutter开发之异步编程

2,819 阅读11分钟

说到网络与通信,就不得不提到异步编程。所谓异步编程,就是一种非阻塞的、事件驱动的编程机制,它可以充分利用系统资源来并行执行多个任务,因此提高了系统的运行效率。

事件循环机制

事件循环是Dart中处理事件的一种机制,与Android中的Handler消息传递机制和前端的eventloop事件循环机制有点类似。在Flutter开发中,Flutter就是通过事件循环来驱动程序运行的。 众所周知,Dart是一种单线程模型运行语言,这意味着Dart在同一时刻只能执行一个操作,其他操作需要在该操作执行完成之后才能执行,而多个操作的执行需要通过Dart的事件驱动模型,其运行流程如下图所示。

在这里插入图片描述
入口main()函数执行完成之后,消息循环机制便启动了。Dart程序在启动时会创建两个队列,一个是微任务队列,另一个是事件队列,并且微任务队列的执行优先级高于事件队列。 首先,事件循环模型会按照先进先出的顺序逐个执行微任务队列中的任务,当所有微任务队列执行完后便开始执行事件队列中的任务,事件任务执行完毕后再去执行微任务,如此循环往复,直到应用退出。 在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务则通常来源于Dart内部,并且微任务非常少。之所以如此,是因为微任务队列优先级高,如果微任务太多,那么执行时间总和就越久,事件队列任务的延迟也就越久。而对于GUI应用来说,最直观的表现就是比较卡,所以Dart的事件循环模型必须保证微任务队列不能太耗时。 由于Dart是一种单线程模型语言,所以当某个任务发生异常且没有被捕获时,程序并不会退出,而是直接阻塞当前任务后续代码的执行,但是并不会阻塞其他任务的执行,也就是说一个任务的异常是不会影响其它任务的执行。 可以看出,将任务加入到微任务中可以被尽快执行,但也需要注意,当事件循环在处理微任务队列时,事件队列会被卡住,此时应用程序无法处理鼠标单击、I/O消息等事件。同时,当事件循坏出现异常时,也可以使用Dart提供的try/catch/finally来捕获异常,并跳过异常执行其他事件。

Isolate

在Flutter开发中,经常会遇到耗时操作的场景,由于Dart是基于单线程模型的语言,所以耗时操作往往会堵塞其他代码的执行。为了解决这一问题,Dart提供了并发机制,即Isolate。 所谓Isolate,其实是Dart中的一个线程,不过与Java中的线程实现方式有所不同,Isolate是通过Flutter的Engine层创建出来的,Dart代码默认运行在主的Isolate上。当Dart代码处于运行状态时,同一个Isolate中的其他代码是无法运行的。Flutter可以拥有多个Isolates,但多个Isolates之间不能共享内存,不同Isolate之间可以通过消息机制来进行通信。 同时,每个Isolate都拥有属于自己的事件循环及消息队列,这意味着在一个Isolate中运行的代码与另外一个Isolate中的代码不存在任何关联。也正是因为这一特性,才让Dart具有了并行处理的能力。 默认情况下,Isolate是通过Flutter的Engine层创建出来的,Dart代码默认运行在主Isolate上,必要时还可以使用系统提供的API来创建新的Isolate,以便更好的利用系统资源,如主线程过载时。 在Dart中,创建Isolate主要有spawnUri和spawn两种方式。与Isolate相关的代码都在isolate.dart文件中,spawnUri的构造函数如下所示。

external static Future<Isolate> spawnUri(
      Uri uri,
      List<String> args,
      var message,
      {bool paused: false,
      SendPort onExit,
      SendPort onError,
      bool errorsAreFatal,
      bool checked,
      Map<String, String> environment,
      @Deprecated('The packages/ dir is not supported in Dart 2')
          Uri packageRoot,
      Uri packageConfig,
      bool automaticPackageResolution: false,
      @Since("2.3")
          String debugName});

使用spawnUri方式创建Isolate时有三个必传参数,分别是Uri、args和messag。其中,Uri用于指定一个新Isolate代码文件的路径,args用于表示参数列表,messag表示需要发送的动态消息。 需要注意的是,用于运行新Isolate的代码文件必须包含一个main函数,它是新创建的Isolate的入口方法,并且main函数中的args参数与spawnUri中的args参数对应。如果不需要向新的Isolate中传递参数,可以向该参数传递一个空列表。首先,使用IntelliJ IDEA新建一个Dart工程,然后在主Isolate中添加如下代码。

import 'dart:isolate';

void main(List<String> arguments) {
  print("main isolate start");
  createIsolate();
  print("main isolate stop");
}

createIsolate() async{
  ReceivePort rp = new ReceivePort();
  SendPort port = rp.sendPort;
  Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_isolate.dart"), ["hello Isolate", "this is args"], port);
  SendPort sendPort;
  rp.listen((message){
    print("main isolate message: $message");
    if (message[0] == 0){
      sendPort = message[1];
    }else{
      sendPort?.send([1,"这条信息是main Isolate发送的"]);
    }
  });
}

然后,在主Isolate文件的同级目录下新建一个other_isolate.dart文件,代码如下。

import 'dart:isolate';
import  'dart:io';

void main(args, SendPort sendPort) {
  print("child isolate start");
  print("child isolate args: $args");
  ReceivePort receivePort = new ReceivePort();
  SendPort port = receivePort.sendPort;
  receivePort.listen((message){
    print("child_isolate message: $message");
  });

  sendPort.send([0, port]);
  sleep(Duration(seconds:5));
  sendPort.send([1, "child isolate 任务完成"]);
  print("child isolate stop");
}

运行主Isolate文件代码,最终的输出结果如下。

main isolate start
main isolate stop
child isolate start
child isolate args: [hello Isolate, this is args]
main isolate message: [0, SendPort]
child isolate stop
main isolate message: [1, child isolate 任务完成]
child_isolate message: [1, 这条信息是main Isolate发送的]

在Dart中,多个Isolate之间的通信是通过ReceivePort来完成的。而ReceivePort可以认为是消息管道,当消息的传递方向时固定的,通过这个管道就能把消息发送给接收端。 除了使用spawnUri外,更常用的方式是使用spawn来创建Isolate,spawn的构造函数如下。

external static Future<Isolate> spawn<T>(
      void entryPoint(T message), T message,
      {bool paused: false,
      bool errorsAreFatal,
      SendPort onExit,
      SendPort onError});

使用spawn方式创建Isolate时需要传递两个参数,即函数entryPoint和参数message。entryPoint表示新创建的Isolate的耗时函数,message表示是动态消息,该参数通常用于传送主Isolate的SendPort对象。 通常,使用spawn方式创建Isolate时,我们希望将新创建的Isolate代码和主Isolate代码写在同一个文件,且不希望出现两个main函数,并且将耗时函数运行在新的Isolate,这样做的目的是有利于代码的组织与复用。

import 'dart:isolate';

Future<void> main(List<String> arguments) async {
  print(await asyncFibonacci(20));     //计算20的阶乘
}

Future<dynamic> asyncFibonacci(int n) async{
  final response = new ReceivePort();
  await Isolate.spawn(isolate,response.sendPort);
  final sendPort = await response.first as SendPort;
  final answer = new ReceivePort();
  sendPort.send([n,answer.sendPort]);
  return answer.first;
}

void isolate(SendPort initialReplyTo){
  final port = new ReceivePort();
  initialReplyTo.send(port.sendPort);
  port.listen((message){
    final data = message[0] as int;
    final send = message[1] as SendPort;
    send.send(syncFibonacci(data));
  });
}

int syncFibonacci(int n){
  return n < 2 ? n : syncFibonacci(n-2) + syncFibonacci(n-1);
}

在上面的代码中,耗时的操作放在使用spawn方法创建的Isolate中。运行上面的程序,最终的输出结果为6765,即20的阶乘。

Flutter线程管理与Isolate

默认情况下,Flutter Engine层会创建一个Isolate,并且Dart代码默认就运行在这个主Isolate上。必要时可以使用spawnUri和spawn两种方式来创建新的Isolate,在Flutter中,新创建的Isolate由Flutter进行统一的管理。 事实上,Flutter Engine自己不创建和管理线程,Flutter Engine线程的创建和管理是Embeder负责的,Embeder指的是将引擎移植到平台的中间层代码,Flutter Engine层的架构示意图如下图所示。

在这里插入图片描述

在Flutter的架构中,Embeder提供四个Task Runner,分别是Platform Task Runner、UI Task Runner Thread、GPU Task Runner和IO Task Runner,每个Task Runner负责不同的任务,Flutter Engine不在乎Task Runner运行在哪个线程,但是它需要线程在整个生命周期里面保持稳定。

Platform Task Runner

Platform Task Runner是Flutter Engine的主Task Runner,类似于Android或者iOS的Main Thread。不过它们之间还是有区别的,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用。 同时,跟Flutter Engine的所有交互都必须在Platform Thread中进行,如果试图在其它线程中调用Flutter Engine可能会出现无法预期的异常,这跟iOS和Android中对于UI的操作都必须发生在主线程的道理类似。需要注意的是,Flutter Engine中有很多模块都是非线程安全的,因此对于Flutter Engine的接口调用都需保证在Platform Thread进行。 虽然阻塞Platform Thread不会直接导致Flutter应用的卡顿,但是也不建议在这个主Runner执行繁重的操作,因为长时间卡住Platform Thread有可能会被系统的Watchdog程序强杀。

UI Task Runner

UI Task Runner用于执行Root Isolate代码,它运行在线程对应平台的线程上,属于子线程。同时,Root isolate在引擎启动时会绑定了不少Flutter需要的函数方法,以便进行渲染操作。 对于每一帧,引擎通过Root Isolate通知Flutter Engine有帧需要渲染,平台收到Flutter Engine通知后会创建对象和组件并生成一个Layer Tree,然后将生成的Layer Tree提交给Flutter Engine。此时,只生成了需要绘制的内容,并没有执行屏幕渲染,而Root Isolate就是负责将创建的Layer Tree绘制到屏幕上,因此如果线程过载会导致卡顿掉帧。 除了用于处理渲染之外,Root Isolate还需要处理来自Native Plugins的消息响应、Timers、MicroTasks和异步IO。如果确实有无法避免的繁重计算,建议将这些耗时的操作放到独立的Isolate去执行,从而避免应用UI卡顿问题。

GPU Task Runner

GPU Task Runner用于执行设备GPU指令,UI Task Runner创建的Layer Tree是跨平台的。也就是说,Layer Tree提供了绘制所需要的信息,但是由由谁来完成绘制它是不关心的。 GPU Task Runner负责将Layer Tree提供的信息转化为平台可执行的GPU指令,GPU Task Runner同时也负责管理每一帧绘制所需要的GPU资源,包括平台Framebuffer的创建,Surface生命周期管理,以及Texture和Buffers的绘制时机等。 一般来说,UI Runner和GPU Runner运行在不同的线程。GPU Runner会根据目前帧执行的进度去向UI Runner请求下一帧的数据,在任务繁重的时候还可能会出现UI Runner的延迟任务。不过这种调度机制的好处在于,确保GPU Runner不至于过载,同时也避免了UI Runner不必要的资源消耗。 GPU Runner可以导致UI Runner的帧调度的延迟,GPU Runner的过载会导致Flutter应用的卡顿,因此在实际使用过程中,建议为每一个Engine实例都新建一个专用的GPU Runner线程。

IO Task Runner

IO Task Runner也运行在平台对应的子线程中,主要作用是做一些预先处理的读取操作,为GPU Runner的渲染操作做准备。我们可以认为IO Task Runner是GPU Task Runner的助手,它可以减少GPU Task Runner的额外工作。例如,在Texture的准备过程中,IO Runner首先会读取压缩的图片二进制数据,并将其解压转换成GPU能够处理的格式,然后再将数据传递给GPU进行渲染。 虽然IO Task Runner并不会直接导致Flutter应用的卡顿,但是可能会导致图片和其它一些资源加载的延迟,并间接影响应用性能,所以建议将IO Runner放到一个专用的线程中。 Dart的Isolate是Dart虚拟机创建和管理的,Flutter Engine无法直接访问。Root Isolate通过Dart的C++调用能力把UI渲染相关的任务提交到UI Runner执行, 这样就可以跟Flutter Engine模块进行交互,Flutter UI的任务也被提交到UI Runner,并可以给Isolate发送一些事件通知,UI Runner同时也可以处理来自应用的Native Plugin任务。

总的来说,Dart Isolate跟Flutter Runner是相互独立的,它们通过任务调度机制相互协作。