咱们知道Flutter 框架有出色的渲染和交互能力。支撑起这些复杂的能力背后,其实是基于单线程模型的 Dart。那么,与原生 Android 和 iOS 的多线程机制相比,单线程的 Dart 如何从语言设计层面和代码运行机制上保证 Flutter UI 的流畅性呢?web
咱们从下面几个方面阐述一下:编程
dart是单线程运行的。怎么理解这句话呢, 从下面几个方面能够看到这个设计思想.bash
dart默认运行在Main函数存在线程,在dart中称之为isolate,这个线程咱们可称之为main isolate。单线程任务处理的,若是不开启新的isolate,任务默认在主isolate中处理。一旦 Dart 函数执行,它将按照在 main 函数出现的次序一个接一个地持续执行,直到退出。换而言之,Dart 函数在执行期间,没法被其余 Dart 代码打断。网络
Android和IOS能够自由的开辟除了UI主线程以外的线程,这些线程和主线程能够共享内存的变量,可是, Dart中的isolate没法共享内存。Isolate 不能共享内存,他们就像是单独的分离的 app,经过消息进行沟通。除了显式指定代码运行在别的 isolate 或者 worker 中,其余代码都运行在 app 的 main isolate 中。更多信息能够访问Use isolates or workers if necessary多线程
(1)假若有一个任务(读写文件或者网络)耗时10秒,而且加入到了事件任务队列中,执行单这个任务的时候不就把线程卡主吗?并发
答:文件I/O和网络调用并非在Dart层作的,而是由操做系统提供的异步线程,他俩把活儿干完以后把结果刚到队列中,Dart代码只是执行一个简单的读动做。app
(2)单线程模型是指的事件队列模型,和绘制界面的线程是一个吗?框架
答:咱们所说的单线程指的是主Isolate。而GPU绘制指令有单独的线程执行,跟主Isolate无关。事实上Flutter提供了4种task runner,有独立的线程去运行专属的任务:参见:深刻理解Flutter引擎线程模式异步
如图所示,dart也存在事件队列和事件循环。每一个isolate也包含一个事件循环,区别是他有两个事件队列,event loop事件循环,以及event queue和microtask queue事件队列,event和microtask队列有点相似iOS的source0和source1。async
MicroTask
队列是否为空,非空则先执行MicroTask
队列中的MicroTaskMicroTask
执行完后,检查有没有下一个MicroTask
,直到MicroTask
队列为空,才去执行Event
队列Evnet
队列取出一个事件处理完后,再次返回第一步,去检查MicroTask
队列是否为空咱们能够看出,将任务加入到MicroTask
中能够被尽快执行,但也须要注意,当事件循环在处理MicroTask
队列时,会阻塞event队列的事件执行,这样就会致使渲染、手势响应等event事件响应延时。为了保证渲染和手势响应,应该尽可能将耗时操做放在event队列中。
咱们一般不多会直接用到微任务队列,就连 Flutter 内部,也只有 7 处用到了而已(好比,手势识别、文本输入、滚动视图、保存页面效果等须要高优执行任务的场景)。
简单总结为一二一模型:1个事件循环和2个队列的单线程执行模型。
为何单线程也能够异步?这里有一个大前提,那就是咱们的 App 绝大多数时间都在等待。好比,等用户点击、等网络请求返回、等文件 IO 结果,等等。而这些等待行为并非阻塞的。好比说,网络请求,Socket 自己提供了 select 模型能够异步查询;而文件 IO,操做系统也提供了基于事件的回调机制。因此,基于这些特色,单线程模型能够在等待的过程当中作别的事情,等真正须要响应结果了,再去作对应的处理。由于等待过程并非阻塞的,因此给咱们的感受就像是同时在作多件事情同样。但其实始终只有一个线程在处理你的事情。
异步任务咱们用的最多的仍是优先级更低的 Event Queue。好比,I/O、绘制、定时器这些异步事件,都是经过事件队列驱动主线程执行的。
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复制代码
Future 是异步任务的封装,借助于 await 与 async,咱们能够经过事件循环实现非阻塞的同步等待。Dart 中的 await 并非阻塞等待,而是异步等待。Dart 会将调用体的函数也视做异步函数,将等待语句的上下文放入 Event Queue 中,一旦有告终果,Event Loop 就会把它从 Event Queue 中取出,等待代码继续执行。
将async
关键字做为方法声明的后缀时,具备以下意义
Future
对象做为返回值// 导入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!
因为 await 是采用事件队列的机制实现等待行为的,因此比它先在事件队列中的 f4 并不会被它阻塞。
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对象只能单向发消息,这就如同一根自来水管,ReceivePort
和SendPort
分别位于水管的两头,水流只能从SendPort
这头流向ReceivePort
这头。所以,两个Isolate
之间的消息通讯确定是须要两根这样的水管的,这就须要两对Port对象。