Flutter中Dart异步模型

前言

咱们知道Flutter 框架有出色的渲染和交互能力。支撑起这些复杂的能力背后,其实是基于单线程模型的 Dart。那么,与原生 Android 和 iOS 的多线程机制相比,单线程的 Dart 如何从语言设计层面和代码运行机制上保证 Flutter UI 的流畅性呢?web

单线程模型

咱们从下面几个方面阐述一下:编程

  1. Dart 语言单线程模型和 Event Loop 处理机制
  2. 异步处理和并发编程的原理和使用方法
  3. Dart 单线程模型下的代码运行本质

1. Dart单线程模型

dart是单线程运行的。怎么理解这句话呢, 从下面几个方面能够看到这个设计思想.bash

1.1 默认单一运行的线程

dart默认运行在Main函数存在线程,在dart中称之为isolate,这个线程咱们可称之为main isolate。单线程任务处理的,若是不开启新的isolate,任务默认在主isolate中处理。一旦 Dart 函数执行,它将按照在 main 函数出现的次序一个接一个地持续执行,直到退出。换而言之,Dart 函数在执行期间,没法被其余 Dart 代码打断。网络

1.2 独享内存

Android和IOS能够自由的开辟除了UI主线程以外的线程,这些线程和主线程能够共享内存的变量,可是, Dart中的isolate没法共享内存。Isolate 不能共享内存,他们就像是单独的分离的 app,经过消息进行沟通。除了显式指定代码运行在别的 isolate 或者 worker 中,其余代码都运行在 app 的 main isolate 中。更多信息能够访问Use isolates or workers if necessary多线程

1.3 质疑

(1)假若有一个任务(读写文件或者网络)耗时10秒,而且加入到了事件任务队列中,执行单这个任务的时候不就把线程卡主吗?并发

答:文件I/O和网络调用并非在Dart层作的,而是由操做系统提供的异步线程,他俩把活儿干完以后把结果刚到队列中,Dart代码只是执行一个简单的读动做。app

(2)单线程模型是指的事件队列模型,和绘制界面的线程是一个吗?框架

答:咱们所说的单线程指的是主Isolate。而GPU绘制指令有单独的线程执行,跟主Isolate无关。事实上Flutter提供了4种task runner,有独立的线程去运行专属的任务:参见:深刻理解Flutter引擎线程模式异步

  1. Platform Task Runner:处理来自平台(Android/iOS)的消息
  2. UI Task Runner:执行渲染逻辑、处理native plugin的消息、timer、microtask、异步I/O操做处理等
  3. GPU Task Runner:执行GPU指令
  4. IO Task Runner:执行I/O任务

2. Event Loop 机制

消息队列模型

如图所示,dart也存在事件队列和事件循环。每一个isolate也包含一个事件循环,区别是他有两个事件队列,event loop事件循环,以及event queue和microtask queue事件队列,event和microtask队列有点相似iOS的source0和source1。async

  • event queue:负责处理I/O事件、绘制事件、手势事件、接收其余isolate消息等外部事件。
  • microtask queue:能够本身向isolate内部添加事件,事件的优先级比event queue高。
事件队列模型
  1. 先检查MicroTask队列是否为空,非空则先执行MicroTask队列中的MicroTask
  2. 一个MicroTask执行完后,检查有没有下一个MicroTask,直到MicroTask队列为空,才去执行Event队列
  3. Evnet 队列取出一个事件处理完后,再次返回第一步,去检查MicroTask队列是否为空

咱们能够看出,将任务加入到MicroTask中能够被尽快执行,但也须要注意,当事件循环在处理MicroTask队列时,会阻塞event队列的事件执行,这样就会致使渲染、手势响应等event事件响应延时。为了保证渲染和手势响应,应该尽可能将耗时操做放在event队列中。

咱们一般不多会直接用到微任务队列,就连 Flutter 内部,也只有 7 处用到了而已(好比,手势识别、文本输入、滚动视图、保存页面效果等须要高优执行任务的场景)。

简单总结为一二一模型:1个事件循环和2个队列的单线程执行模型。

3. 异步任务调度

为何单线程也能够异步?这里有一个大前提,那就是咱们的 App 绝大多数时间都在等待。好比,等用户点击、等网络请求返回、等文件 IO 结果,等等。而这些等待行为并非阻塞的。好比说,网络请求,Socket 自己提供了 select 模型能够异步查询;而文件 IO,操做系统也提供了基于事件的回调机制。因此,基于这些特色,单线程模型能够在等待的过程当中作别的事情,等真正须要响应结果了,再去作对应的处理。由于等待过程并非阻塞的,因此给咱们的感受就像是同时在作多件事情同样。但其实始终只有一个线程在处理你的事情。

异步任务咱们用的最多的仍是优先级更低的 Event Queue。好比,I/O、绘制、定时器这些异步事件,都是经过事件队列驱动主线程执行的。

3.1 用Future发起异步任务

Dart 为 Event Queue 的任务创建提供了一层封装,叫做 Future。Future 还提供了链式调用的能力,能够在异步任务执行完毕后依次执行链路上的其余函数体。

new Future((){
    //  doing something
});复制代码

微任务是由 scheduleMicroTask 创建的。以下所示,这段代码会在下一个事件循环中输出一段字符串:

scheduleMicrotask(() => print('This is a microtask'));复制代码

链式调用:

Future(() => print('Running in Future 1'));//下一个事件循环输出字符串

Future(() => print(‘Running in Future 2')) .then((_) => print('and then 1')) .then((_) => print('and then 2’));//上一个事件循环结束后,连续输出三段字符串复制代码

Dart 会将异步任务的函数执行体放入事件队列,而后当即返回,后续的代码继续同步执行。而当同步执行的代码执行完毕后,事件队列会按照加入事件队列的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的 then。这意味着,then 与 Future 函数体共用一个事件循环。而若是 Future 有多个 then,它们也会按照链式调用的前后顺序同步执行,一样也会共用一个事件循环。

若是 Future 执行体已经执行完毕了,但你又拿着这个 Future 的引用,往里面加了一个 then 方法体,这时 Dart 会如何处理呢?面对这种状况,Dart 会将后续加入的 then 方法体放入微任务队列,尽快执行。

//f1比f2先执行
Future(() => print('f1'));
Future(() => print('f2'));

//f3执行后会马上同步执行then 3
Future(() => print('f3')).then((_) => print('then 3'));

//then 4会加入微任务队列,尽快执行
Future(() => null).then((_) => print('then 4'));
结果: f1 f2 f3 then 3 then 4复制代码

4. 异步函数

Future 是异步任务的封装,借助于 await 与 async,咱们能够经过事件循环实现非阻塞的同步等待。Dart 中的 await 并非阻塞等待,而是异步等待。Dart 会将调用体的函数也视做异步函数,将等待语句的上下文放入 Event Queue 中,一旦有告终果,Event Loop 就会把它从 Event Queue 中取出,等待代码继续执行。

async关键字做为方法声明的后缀时,具备以下意义

  • 被修饰的方法会将一个 Future 对象做为返回值
  • 该方法会同步执行其中的方法的代码直到第一个 await 关键字,而后它暂停该方法其余部分的执行;
  • 一旦由 await 关键字引用的 Future 任务执行完成,await的下一行代码将当即执行。
// 导入io库,调用sleep函数
import 'dart:io';

// 模拟耗时操做,调用sleep函数睡眠2秒
doTask() async{
  await sleep(const Duration(seconds:2));
  return "Ok";
}

// 定义一个函数用于包装
test() async {
  var r = await doTask();
  print(r);
}

void main(){
  print("main start");
  test();
  print("main end");
}
结果:
main start
main end
Ok复制代码

咱们先来看下这段代码。第二行的 then 执行体 f2 是一个 Future,为了等它完成再进行下一步操做,咱们使用了 await,指望打印结果为 f一、f二、f三、f4:

Future(()=>print('f1'))
.then((_)async=>awaitFuture(()=>print('f2')))
.then((_)=>print('f3'));
Future(()=>print('f4'));复制代码

实际上,当你运行这段代码时就会发现,打印出来的结果实际上是 f一、f四、f二、f3!

  • 分析一下这段代码的执行顺序:
  • 按照任务的声明顺序,f1 和 f4 被前后加入事件队列。
  • f1 被取出并打印;
  • 而后到了 then。then 的执行体是个 future f2,因而放入 Event Queue。
  • 而后把 await 也放到 Event Queue 里。这个时候要注意了,Event Queue 里面还有一个 f4,咱们的 await 并不能阻塞 f4 的执行。所以,Event Loop 先取出 f4,打印 f4;
  • 而后才能取出并打印 f2,最后把等待的 await 取出,开始执行后面的 f3。

因为 await 是采用事件队列的机制实现等待行为的,因此比它先在事件队列中的 f4 并不会被它阻塞。

5. Isolate

Dart 也提供了多线程机制,即 Isolate(这个单词的中文意思是隔离)。在 Isolate 中,资源隔离作得很是好,每一个 Isolate 都有本身的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通讯,所以也就没有资源抢占问题。以下所示,咱们声明了一个 Isolate 的入口函数,而后在 main 函数中启动它,并传入了一个字符串参数:

doSth(msg) => print(msg);

main() {
  Isolate.spawn(doSth, "Hi");
  ...
}复制代码

那么如何利用消息机制进行通讯呢,下面引用了一篇文章的讲解,图画的很好。

引用

整个消息通讯过程如上图所示,两个Isolate是经过两对Port对象通讯,一对Port分别由用于接收消息的ReceivePort对象,和用于发送消息的SendPort对象构成。其中SendPort对象不用单首创建,它已经包含在ReceivePort对象之中。须要注意,一对Port对象只能单向发消息,这就如同一根自来水管,ReceivePortSendPort分别位于水管的两头,水流只能从SendPort这头流向ReceivePort这头。所以,两个Isolate之间的消息通讯确定是须要两根这样的水管的,这就须要两对Port对象。

6. 引用文章

(1)23 | 单线程模型怎么保证UI运行流畅?

(2)Dart 异步编程详解之一文全懂

(3)Dart asynchronous programming: Isolates and event loops

相关文章
相关标签/搜索