深刻理解Flutter多线程

该文章属于<简书 — 刘小壮>原创,转载请注明:

<简书 — 刘小壮> https://www.jianshu.com/p/54da18ed1a9egit


封面图

Flutter默认是单线程任务处理的,若是不开启新的线程,任务默认在主线程中处理。github

事件队列

和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫作isolate。应用程序启动后,开始执行main函数并运行main isolate编程

每一个isolate包含一个事件循环以及两个事件队列,event loop事件循环,以及event queuemicrotask queue事件队列,eventmicrotask队列有点相似iOS的source0source1json

  • event queue:负责处理I/O事件、绘制事件、手势事件、接收其余isolate消息等外部事件。
  • microtask queue:能够本身向isolate内部添加事件,事件的优先级比event queue高。

事件队列

这两个队列也是有优先级的,当isolate开始执行后,会先处理microtask的事件,当microtask队列中没有事件后,才会处理event队列中的事件,并按照这个顺序反复执行。但须要注意的是,当执行microtask事件时,会阻塞event队列的事件执行,这样就会致使渲染、手势响应等event事件响应延时。为了保证渲染和手势响应,应该尽可能将耗时操做放在event队列中。安全

async、await

在异步调用中有三个关键词,asyncawaitFuture,其中asyncawait须要一块儿使用。在Dart中能够经过asyncawait进行异步操做,async表示开启一个异步操做,也能够返回一个Future结果。若是没有返回值,则默认返回一个返回值为nullFuture网络

asyncawait本质上就是Dart对异步操做的一个语法糖,能够减小异步调用的嵌套调用,而且由async修饰后返回一个Future,外界能够以链式调用的方式调用。这个语法是JSES7标准中推出的,Dart的设计和JS相同。架构

下面封装了一个网络请求的异步操做,而且将请求后的Response类型的Future返回给外界,外界能够经过await调用这个请求,并获取返回数据。从代码中能够看到,即使直接返回一个字符串,Dart也会对其进行包装并成为一个Future并发

Future<Response> dataReqeust() async {
    String requestURL = 'https://jsonplaceholder.typicode.com/posts';
    Client client = Client();
    Future<Response> response = client.get(requestURL);
    return response;
}

Future<String> loadData() async {
    Response response = await dataReqeust();
    return response.body;
}
复制代码

在代码示例中,执行到loadData方法时,会同步进入方法内部进行执行,当执行到await时就会中止async内部的执行,从而继续执行外面的代码。当await有返回后,会继续从await的位置继续执行。因此await的操做,不会影响后面代码的执行。框架

下面是一个代码示例,经过async开启一个异步操做,经过await等待请求或其余操做的执行,并接收返回值。当数据发生改变时,调用setState方法并更新数据源,Flutter会更新对应的Widget节点视图。异步

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}
复制代码

Future

Future就是延时操做的一个封装,能够将异步任务封装为Future对象。获取到Future对象后,最简单的方法就是用await修饰,并等待返回结果继续向下执行。正如上面async、await中讲到的,使用await修饰时须要配合async一块儿使用。

Dart中,和时间相关的操做基本都和Future有关,例如延时操做、异步操做等。下面是一个很简单的延时操做,经过Futuredelayed方法实现。

loadData() {
    // DateTime.now(),获取当前时间
    DateTime now = DateTime.now();
    print('request begin $now');
    Future.delayed(Duration(seconds: 1), (){
      now = DateTime.now();
      print('request response $now');
    });
}
复制代码

Dart还支持对Future的链式调用,经过追加一个或多个then方法来实现,这个特性很是实用。例如一个延时操做完成后,会调用then方法,而且能够传递一个参数给then。调用方式是链式调用,也就表明能够进行不少层的处理。这有点相似于iOS的RAC框架,链式调用进行信号处理。

Future.delayed(Duration(seconds: 1), (){
  int age = 18;
  return age;
}).then((onValue){
  onValue++;
  print('age $onValue');
});
复制代码

协程

若是想要了解asyncawait的原理,就要先了解协程的概念,asyncawait本质上就是协程的一种语法糖。协程,也叫做coroutine,是一种比线程更小的单元。若是从单元大小来讲,基本能够理解为进程->线程->协程。

任务调度

在弄懂协程以前,首先要明白并发和并行的概念,并发指的是由系统来管理多个IO的切换,并交由CPU去处理。并行指的是多核CPU在同一时间里执行多个任务。

并发的实现由非阻塞操做+事件通知来完成,事件通知也叫作“中断”。操做过程分为两种,一种是CPU对IO进行操做,在操做完成后发起中断告诉IO操做完成。另外一种是IO发起中断,告诉CPU能够进行操做。

线程本质上也是依赖于中断来进行调度的,线程还有一种叫作“阻塞式中断”,就是在执行IO操做时将线程阻塞,等待执行完成后再继续执行。但线程的消耗是很大的,并不适合大量并发操做的处理,而经过单线程并发能够进行大量并发操做。当多核CPU出现后,单个线程就没法很好的利用多核CPU的优点了,因此又引入了线程池的概念,经过线程池来管理大量线程。

协程

在程序执行过程当中,离开当前的调用位置有两种方式,继续调用其余函数和return返回离开当前函数。可是执行return时,当前函数在调用栈中的局部变量、形参等状态则会被销毁。

协程分为无线协程和有线协程,无线协程在离开当前调用位置时,会将当前变量放在堆区,当再次回到当前位置时,还会继续从堆区中获取到变量。因此,通常在执行当前函数时就会将变量直接分配到堆区,而asyncawait就属于无线协程的一种。有线协程则会将变量继续保存在栈区,在回到指针指向的离开位置时,会继续从栈中取出调用。

async、await原理

asyncawait为例,协程在执行时,执行到async则表示进入一个协程,会同步执行async的代码块。async的代码块本质上也至关于一个函数,而且有本身的上下文环境。当执行到await时,则表示有任务须要等待,CPU则去调度执行其余IO,也就是后面的代码或其余协程代码。过一段时间CPU就会轮训一次,看某个协程是否任务已经处理完成,有返回结果能够被继续执行,若是能够被继续执行的话,则会沿着上次离开时指针指向的位置继续执行,也就是await标志的位置。

因为并无开启新的线程,只是进行IO中断改变CPU调度,因此网络请求这样的异步操做可使用asyncawait,但若是是执行大量耗时同步操做的话,应该使用isolate开辟新的线程去执行。

若是用协程和iOS的dispatch_async进行对比,能够发现两者是比较类似的。从结构定义来看,协程须要将当前await的代码块相关的变量进行存储,dispatch_async也能够经过block来实现临时变量的存储能力。

我以前还在想一个问题,苹果为何不引入协程的特性呢?后来想了一下,awaitdispatch_async均可以简单理解为异步操做,OC的线程是基于Runloop实现的,Dart本质上也是有事件循环的,并且两者都有本身的事件队列,只是队列数量和分类不一样。

我以为当执行到await时,保存当前的上下文,并将当前位置标记为待处理任务,用一个指针指向当前位置,并将待处理任务放入当前isolate的队列中。在每一个事件循环时都去询问这个任务,若是须要进行处理,就恢复上下文进行任务处理。

Promise

这里想提一下JS里的Promise语法,在iOS中会出现不少if判断或者其余的嵌套调用,而Promise能够把以前横向的嵌套调用,改为纵向链式调用。若是能把Promise引入到OC里,可让代码看起来更简洁,直观。

isolate

isolateDart平台对线程的实现方案,但和普通Thread不一样的是,isolate拥有独立的内存,isolate由线程和独立内存构成。正是因为isolate线程之间的内存不共享,因此isolate线程之间并不存在资源抢夺的问题,因此也不须要锁。

经过isolate能够很好的利用多核CPU,来进行大量耗时任务的处理。isolate线程之间的通讯主要经过port来进行,这个port消息传递的过程是异步的。经过Dart源码也能够看出,实例化一个isolate的过程包括,实例化isolate结构体、在堆中分配线程内存、配置port等过程。

isolate看起来其实和进程比较类似,以前请教阿里架构师宗心问题时,宗心也说过“isolate的总体模型我本身的理解其实更像进程,而asyncawait更像是线程”。若是对比一下isolate和进程的定义,会发现确实isolate很像是进程。

代码示例

下面是一个isolate的例子,例子中新建立了一个isolate,而且绑定了一个方法进行网络请求和数据解析的处理,并经过port将处理好的数据返回给调用方。

loadData() async {
    // 经过spawn新建一个isolate,并绑定静态方法
    ReceivePort receivePort =ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);
    
    // 获取新isolate的监听port
    SendPort sendPort = await receivePort.first;
    // 调用sendReceive自定义方法
    List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
    print('dataList $dataList');
}

// isolate的绑定方法
static dataLoader(SendPort sendPort) async{
    // 建立监听port,并将sendPort传给外界用来调用
    ReceivePort receivePort =ReceivePort();
    sendPort.send(receivePort.sendPort);
    
    // 监听外界调用
    await for (var msg in receivePort) {
      String requestURL =msg[0];
      SendPort callbackPort =msg[1];
    
      Client client = Client();
      Response response = await client.get(requestURL);
      List dataList = json.decode(response.body);
      // 回调返回值给调用者
      callbackPort.send(dataList);
    }    
}

// 建立本身的监听port,而且向新isolate发送消息
Future sendReceive(SendPort sendPort, String url) {
    ReceivePort receivePort =ReceivePort();
    sendPort.send([url, receivePort.sendPort]);
    // 接收到返回值,返回给调用者
    return receivePort.first;
}
复制代码

isolate和iOS中的线程还不太同样,isolate的线程更偏底层。当生成一个isolate后,其内存是各自独立的,相互之间并不能进行访问。但isolate提供了基于port的消息机制,经过创建通讯双方的sendPortreceiveport,进行相互的消息传递,在Dart中叫作消息传递。

从上面例子中能够看出,在进行isolate消息传递的过程当中,本质上就是进行port的传递。将port传递给其余isolate,其余isolate经过port拿到sendPort,向调用方发送消息来进行相互的消息传递。

Embedder

正如其名,Embedder是一个嵌入层,将Flutter嵌入到各个平台上。Embedder负责范围包括原平生台插件、线程管理、事件循环等。

Flutter System Overriew

Embedder中存在四个Runner,四个Runner分别以下。其中每一个Flutter Engine各自对应一个UI RunnerGPU RunnerIO Runner,但全部Engine共享一个Platform Runner

Embedder

Runnerisolate并非一码事,彼此相互独立。以iOS平台为例,Runner的实现就是CFRunLoop,以一个事件循环的方式不断处理任务。而且Runner不仅处理Engine的任务,还有Native Plugin带来的原平生台的任务。而isolate则由Dart VM进行管理,和原平生台线程并没有关系。

Platform Runner

Platform Runner和iOS平台的Main Thread很是类似,在Flutter中除耗时操做外,全部任务都应该放在Platform中,Flutter中的不少API并非线程安全的,放在其余线程中可能会致使一些bug。

但例如IO之类的耗时操做,应该放在其余线程中完成,不然会影响Platform的正常执行,甚至于被watchdog干掉。但须要注意的是,因为Embedder Runner的机制,Platform被阻塞后并不会致使页面卡顿。

不仅是Flutter Engine的代码在Platform中执行,Native Plugin的任务也会派发到Platform中执行。实际上,在原生侧的代码运行在Platform Runner中,而Flutter侧的代码运行在Root Isolate中,若是在Platform中执行耗时代码,则会卡原平生台的主线程。

UI Runner

UI Runner负责为Flutter Engine执行Root Isolate的代码,除此以外,也处理来自Native Plugin的任务。Root Isolate为了处理自身事件,绑定了不少函数方法。程序启动时,Flutter Engine会为Root绑定UI Runner的处理函数,使Root Isolate具有提交渲染帧的能力。

Root IsolateEngine提交一次渲染帧时,Engine会等待下次vsync,当下次vsync到来时,由Root IsolateWidgets进行布局操做,并生成页面的显示信息的描述,并将信息交给Engine去处理。

因为对widgets进行layout并生成layer treeUI Runner进行的,若是在UI Runner中进行大量耗时处理,会影响页面的显示,因此应该将耗时操做交给其余isolate处理,例如来自Native Plugin的事件。

Rendering Pipeline.jpg

GPU Runner

GPU Runner并不直接负责渲染操做,其负责GPU相关的管理和调度。当layer tree信息到来时,GPU Runner将其提交给指定的渲染平台,渲染平台是Skia配置的,不一样平台可能有不一样的实现。

GPU Runner相对比较独立,除了Embedder外其余线程均不可向其提交渲染信息。

Graphics Pipeline

IO Runner

一些GPU Runner中比较耗时的操做,就放在IO Runner中进行处理,例如图片读取、解压、渲染等操做。可是只有GPU Runner才能对GPU提交渲染信息,为了保证IO Runner也具有这个能力,因此IO Runner会引用GPU Runnercontext,这样就具有向GPU提交渲染信息的能力。


简书因为排版的问题,阅读体验并很差,布局、图片显示、代码等不少问题。因此建议到我Github上,下载Flutter编程指南 PDF合集。把全部Flutter文章总计三篇,都写在这个PDF中,并且左侧有目录,方便阅读。

Flutter编程指南

下载地址:Flutter编程指南 PDF 麻烦各位大佬点个赞,谢谢!😁

相关文章
相关标签/搜索