从本篇文章开始,我们将一起进入Flutter中的异步,异步操作可以说在Flutter开发中无处不在。Flutter异步实际上基本就等同于Dart的异步编程,尽管Flutter框架有一些异步的封装,但是本质上还是Dart的异步。本文目的不仅仅学会如何使用Dart中异步API,更重要的是需要去理解其背后的原理。我们都知道「Dart语言和Javascript一样都是单线程模型的,而不是像类似Java、C#多线程模型」。同时也就意味着Dart不存在像Java多线程线程安全以及锁竞争情况,开发也会比较简单清晰。虽然Dart是单线程模型的,但是我们都知道几乎所有客户端页面UI都是在主线程更新的,遇到耗时的任务需要在子线程执行,否则会导致主线程UI的卡顿等一系列问题,所以Dart也不例外。
1. 先来梳理几个概念
在开始正式异步编程讲解之前还是先来梳理回顾几个异步编程中的概念。
1.1 同步(Synchronous)和异步(Asynchronous)
同步和异步一直是异步并发编程中比较重要概念,需要深刻理解它们之间的区别,它们更「侧重于描述不同的流程设计方式」。
同步: 存在依赖顺序关系的操作之间是同步的,比如说B操作是依赖A操作的返回,B操作的执行必须要等到A操作执行之后才能执行。
异步: 操作之间并没有依赖关系的换句话说操作执行的顺序并没有一定严格。比如操作A,B,C并没有依赖联系,哪个先开始执行,那么先执行结束都没有关系。
比如异步情况下,A,B,C三个没有依赖关系它们执行顺序可能有多种,下面只列出两种用于理解。
1.2 阻塞和非阻塞
阻塞和非阻塞更「侧重于描述操作程序是否会立即返回」,这个概念也容易和同步异步搞混。 同步不一定会阻塞主要取决于在同步执行的操作中是否存在有阻塞的操作,而基于单线程的异步API实现里的操作一般都会要求为非阻塞。
1.3 多线程和单线程
我们经常会把同步异步和单线程多线程概念搞混了,认为多线程一定就是异步的,还有就是单线程就一定是同步的。实际上这些理解都是有问题的,比如很多基于EventLoop的单线程异步模型。「单线程和多线程主要侧重的是运行的环境」。其实多线程既可以实现同步也能实现异步,单线程也是如此既可以实现同步也可以实现异步。 从上图就能明显分析出「单线程和多线程只是侧重于程序执行环境不一样,而同步和异步和它没有本质必然的联系。只是说一般用单线程实现同步,多线程实现异步会更简单和更易于理解,实际上单线程和多线程都能实现同步和异步」。
2. Dart中的EventLoop(事件循环)
首先,我们必须要清楚「Dart是单线程模型」的,这也就意味着「Dart一次只能执行一个操作,然后其他操作一个接着一个,只要有一个操作正在执行,它就不能被其他任何操作给中断」。那么是不是就意味着Dart代码只能是同步的,答案肯定不是。实际上Dart是支持单线程非阻塞式异步,支持的背后是因为Dart的事件循环(EventLoop)的驱动。我们都知道在前端开发框架大多数都是事件驱动的,事件驱动也意味着程序中存在事件循环和事件队列, 事件循环会不停的从事件队列中获取和处理各种事件。其实类似事件循环这种机制,在不同技术领域都有体现,比如Android中的 Looper/MessageQueue/Handler
、iOS中的 RunLoop
以及js中的 EventLoop
等等。所以Dart一样也是属于事件驱动,它也有自己的 EventLoop
.
3. EventLoop的组成
Dart的EventLoop事件循环和Javascript很类似,同样循环中拥有两个FIFO队列: 「一个是事件队列(Event Queue),另一个就是微任务队列(MicroTask Queue)」
3.1 事件队列(Event Queue)
主要包含一些外部事件和 Future
,比如IO,绘制,手势以及 Timer
等事件。
- 外部事件: IO、手势、绘制、定时器(Timer)、Stream流等
- Future
实际上,「每一次触发外部事件时,需要执行的相应的代码就会被引用到事件队列中」。「一旦如果没有任何微任务执行的话,EventLoop就会取出事件队列中的第一个事件然后执行它」。值得一提的是** Future
也是通过事件队列进行处理**的,也就意味在Dart一次网络请求触发返回包装的 Future
,最后是否拿到正常数据或异常这个事件最终也是交由事件队列来处理的。
3.2 微任务队列(MicroTask Queue)
主要用于Dart内部的微任务(内部非常短暂操作),一般是通过 scheduleMicroTask
方法实现调度,这些任务「需要在其他内部微任务执行完之后且在把事件移交给事件队列之前」立即异步执行。下面可以通过一个简单的例子(假如需要在资源关闭之后执行某个操作,但是这个资源关闭又需要一定的时间)来看下:
import 'dart:async';
void main() {
release();
}
void release() {
scheduleMicrotask(_dispose);//在资源关闭后执行dispose操作
_close();//关闭资源
}
void _close() {
print('close resource');
}
void _dispose() {
print('dispose run');
}
输出结果:
4. EventLoop的执行流程
当启动一个Dart应用程序或者Flutter应用程序时,将创建并启动一个新的Thread线程(在Dart中不叫Thread,而是isolate), 这个线程将是整个应用的主线程。 所以当这个线程被创建, Dart就会自动执行以下几步:
- 1、初始化两个队列,分别是事件队列和微任务队列
- 2、执行
main
方法 - 3、启动事件循环EventLoop
4.1 两种队列异步执行
在Dart要实现异步代码,只需要把代码加入到微任务队列或者事件队列中就可以了。
微任务队列实现异步
MicroTask Queue实现异步主要就是通过上述所提到的 scheduleMicroTask
方法来实现,参数就是一个异步执行的函数。
import 'dart:async';
void main() {
scheduleMicrotask(() => print('async code in microtask queue'));
print('execute main');
}
输出结果:
事件队列实现异步
Event Queue实现异步任务主要是借助 Timer
类来实现,上述也说了事件队列主要包括事件其中就有 Timer
.
import 'dart:async';
void main() {
Timer.run(() => print('async code in event queue'));
print('execute main');
}
输出结果:
4.2 用一个例子来加深对EventLoop理解
有下面这段代码,分析输出的结果.
import 'dart:async';
void main() {
Timer.run(() => print('async code in event queue'));//async code in event queue这句话是否能输出???
release();
print('execute main');
}
void release() {
scheduleMicrotask(release);
}
输出结果:
分析结果:
为什么 async code in event queue
这句输出,永远不会输出呢?其实分析一下就很清楚了,首先 Timer
是会被添加到事件队列中的,而 release
方法中的 scheduleMicroTask
是会被添加到微任务队列中的,通过上面那种流程图就可以得到「事件队列中的事件执行必须等到微任务队列中所有微任务执行完后,才会取出事件队列事件进行处理」,然而 scheduleMicrotask(release)
类似递归调用 release
方法,始终保持了微任务队列中不为空,那么事件队列中的事件永远得不到处理。所以不会输出async code in event queue
5. 用一个Flutter例子来分析EventLoop的工作流程
比如通过一个Flutter按钮的点击,发出一次网络请求,并拿到网络请求的结果, 具体代码如下:
RaisedButton(
child: Text('Click me'),
onPressed: () {
final myFuture = http.get('https://example.com');
myFuture.then((response) {
if (response.statusCode == 200) {
print('Success!');
}
});
},
)
上述是一个Flutter的简单例子,就是点击一个按钮发出一个网络请求,然后拿到网络请求结果。
- 1、首先,当运行这个Flutter应用程序的时候,Flutter会把这个
RaisedButton
按钮绘制在屏幕上,此时的EventLoop可能处于空闲期或者在处理其他的事件。
- 2、然后,创建好的按钮等待用户点击触发,当用户此时点击了这个
RaisedButton
时,此时会有一个Tap
事件进入事件队列,直到这个事件被EventLoop处理,当被EventLoop处理的时候Flutter的渲染系统会识别出手势点击坐标正好落入了RaisedButton
坐标范围了,这时候Flutter就会执行onPressed
方法,此时Tap
处理完后也就会被丢弃了。
- 3、然后,
onPressed
方法中是启动了一个网络请求并返回一个Future
,并且使用then
方法来注册当Future
完成后的回调监听, 我们上面也说过Future
最终也会被加入事件队列(Event Queue)中,可以这样理解Future
正在等待网络数据也是属于一个特定的事件,直到网络数据到了,此时就会有个特定事件进入事件队列(Event Queue), 最后也会被EventLoop处理。
- 4、最后,
Future
完成后,相应回调方法就会被触发,就会返回正常的response
了,这就是大概分析了下Flutter中一个简单交互操作怎么和EventLoop联系起来的,这个地方需要好好深入理解下。
6. 熊喵先生的小总结
关于异步编程系列的EventLoop事件循环就结束,这篇文章全面介绍了EventLoop工作原理以及它的执行过程,包括了事件队列和微任务队列执行的关系。感谢关注,熊喵先生愿和你在技术路上一起成长!