Flutter开发之异步编程

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

事件循环机制

事件循环是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的构造函数以下。async

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是相互独立的,它们经过任务调度机制相互协做。

相关文章
相关标签/搜索