在阅读本文章之前,我强烈建议大家先看一遍这篇文章:Dart 异步编程:Isolates and event loops,也是与 event loop 有关的知识,内容非常通俗易懂,算是一篇 event loop 的基础导读,看完这篇文章再回来看本文,相信会有更好的理解效果。
在安卓中,每个程序都有一个 Looper,运行在主线程中,这个 Looper 会在执行 main() 函数时进行初始化,在 Looper 初始化的同时还会创建 Message Queue,message queque 用于存储一个个事件,而 Looper 则不断从 Message Queue 中以先进先出的方式取出事件并执行相应的操作。
在安卓里,页面的创建、控件的重绘,线程之间的通信等异步操作都是基于 Looper 来实现的。总的来说一个安卓 APP 的运行,就是由许许多多个不同的事件组成,Flutter APP 亦如此。
更多关于安卓的消息机制的知识,可以参考这一篇文章:源码分析Android的消息机制
Flutter 基于 Dart 语言编写,在 Dart 中也有一个 event loop,它的作用与安卓里的 Looper 类似,也是起到一个执行异步操作的作用,本篇文章会为大家详细介绍 event loop 的知识。
每个 Dart app 都有一个单独的 event loop,并维护着两个队列:分别是 event queue 和 microtask queue。
这个 event loop 在主线程空闲的时候就会开始运作,不停遍历两个队列,以 FIFO 的方式执行队列中的任务。
通过官方给的一张图,来理解 event loop 具体的执行顺序:
event loop 会在 main() 函数执行完毕后执行,先检查 Microtask Queue 队列中是否有等待的任务,有的话就从Queue 取出第一个任务执行直到队列为空,否则检查 Event Queue 队列中是否有等待的任务,有的话同样取出队列中第一个任务执行,取出的任务执行完后,会再次检查 Microtask Queue 队列中是否有等待任务,没有的话才会接着遍历 Event Queue 中下一个等待任务。
一旦两个队列都是空的,并且没有更多的事件发生,应用便会退出。但是在任务执行的过程中也可以插入新的微任务和事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而在 Flutter 中,主线程的执行过程正是如此,永不终止。
当然,如果 app 的用户在 event loop 循环的过程中退出了 app,那么应用也会退出,不会执行队列中剩下的任务。
下面来介绍下这两个队列分别接收什么样的任务。
Microtask Queue
微任务队列,微任务通常来源于 Dart 内部,并且微任务非常少,因为微任务队列的优先级比较高,如果 event loop 检测到 microtask queue 中有时间等待执行会优先执行这里面的任务。如果微任务很多,那么处在Event Queue 中的任务就会因为优先级比较低而等待。
Event Queue
事件队列,所有的外部发生的事件,如 I/O,屏幕的点击、组件的绘画,timers,isolates之间的消息传递等等事件,都会在该队列入队。位于事件队列的任务优先级比较低,假设 event loop 正在执行着事件队列中的任务,如果检测到微任务队列中有等待的任务,则会在执行完当前事件队列中的任务后,优先执行微任务队列中的任务,也就是说 event loop 允许微任务队列插队。
值得留意的是,当 event loop 从微任务队列中执行任务时,事件队列就会阻塞:应用不能绘制图形、处理用户点击、响应I/O操作等等。
也就是说,微任务处理耗时过长的话,会导致应用程序的掉帧,降低用户体验。
如何向两个队列中新增任务?
如果你希望某些代码异步执行,你可以通过以下方式:
- 通过 Future 类,可以将任务添加到事件队列中。
- 通过顶级函数
scheduleMicrotask()
,将任务添加到微任务队列中。
向微任务队列添加任务
// 向微任务队列中添加一个任务
scheduleMicrotask(() {
// ...
});
上面的代码将一个任务添加到了微任务队列的队尾。
也可以使用 Future.microtask()
,这个函数同样也是想微任务队列中添加任务,不过在 Future 中会捕获任务抛出的异常,如果任务执行出错,错误信息会包裹在返回的 Future 中。如果你需要在微任务队列执行的任务需要返回错误信息,那么建议使用 Future 返回。
// 向微任务队列中添加一个任务
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
// 捕获发生的异常
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
向事件队列添加任务
使用 new Future()
或 new Future.delayed()
来将一个任务添加到事件任务的队尾。
// 向事件任务队列中添加一个任务
Future((){
});
// 延迟一秒,向事件队列添加任务
Future.delayed(Duration(seconds: 1), () {
});
这两个函数,内部都是基于 Timer.run()
来向事件队列中添加任务,Future 只是在 Timer 的基础上添加了错误捕获的封装。
也可以直接使用 Timer.run()
来完成,但如果任何未捕获的异常发生了,那么你的程序就会退出,因此推荐使用Future。
并且要补充说明的是,在第二个 Future.delayed()
函数中,我们延迟了1秒向事件队列添加任务,这是个不切确的执行时间,虽然任务在1秒后会被放入到事件队列中,但不能保证它在1秒后会立刻执行。需要等优先级更高的微任务队列为空,以及这个事件任务前面的任务都完成了才会执行。
Future 让任务有序执行
可以使用 then()
函数让异步的 Future 以一定顺序执行,then()
函数返回的依然是个 Future。
Future((){ // (1)
}).then((value) => { // (2)
// 在 Future (1) 成功返回结果后,立刻执行 Future (2), value 是 Future (1) 的结果返回值
// Future (2) 中的代码...
});
then()
方法可以接受两个参数,前一个参数 onValue()
只会在 Future (1) 执行成功时才会调用,第二个参数 onError
是个可选参数,顾名思义,当 Future (1) 出现错误的时候,如果提供了 onError 参数,这个 onError 会被执行,then() 因为发生了错误会被跳过。如果没有提供 onError 参数,那么该错误会直接作为 Future 的返回结果传出。
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
then() 中的 onValue
函数可能会因为错误而得不到处理,如果你希望 Future 在抛出错误的时候,执行必要的操作,你可以通过 Future.whenCmoplete()
,如同 try-catch 中的 finally 一样,Future 结束时 whenComplete() 里的代码一定会得到执行。
Future((){ // (1)
print("执行 Future (1)!");
throw Exception("error in Future (1)");
}).catchError((e) {
print("catchError in Future (1)");
}).whenComplete(() {
print("when complete call.");
});
执行结果如下:
I/flutter (24038): 执行 Future (1)!
I/flutter (24038): catchError in Future (1)
I/flutter (24038): when complete call.
Future 任务的错误处理
Future 的错误处理有三种方式
第一种,直接通过向 then()
传入第二个参数来进行错误处理,只会捕获从 调用者(Future 1) 中抛出的错误。
Future((){ // (1)
}).then((value) => {
}, onError: (e) {
print('then() 中的 onError e: ${e.toString()}');
}
第二种,通过 catchError()
处理,调用链上任意类型的错误都会被捕获。
Future((){ // (1)
print("执行 Future (1)!");
}).catchError((e) {
print('catchError() 中的 onError e: ${e.toString()}');
});
第三种,通过 Future.onError<SomeException>()
处理,捕获调用链上指定类型的异常,SomeExcption 指的是想要捕获的错误类型。
如果 SomeException 传入的是 Object 类型,那么该函数起到的作用与 catchError() 是类似的。不过这个函数一般很少用,这里就不在花篇幅去详细介绍它。
Future((){ // (1)
print("执行 Future (1)!");
}).then((value) { // (2)
print("执行 Future (2)!");
throw HttpException("error in Future (2)");
}, onError: (e) {
print('then() 中的 onError e: ${e.toString()}');
}).then((value) { // (3)
print("执行 Future (3)!");
}).onError<HttpException>((error, stackTrace) {
// 指定捕获的错误泛型为 HttpException,只捕获这一类的错误
print('catchError() 中的 onError e: ${error.toString()}');
});
then()
函数里传入的 onError 参数,它只捕获调用者 Future 的异常。
当 onError 参数 和 catchError() 同时存在时,出现的异常会在哪里捕获呢?
Future(() {
print('future #1a (a new future)');
throw Exception("throws error on Future #1a");
}).then((value) => Future(
() {
print('future #2a (a new future)');
throw Exception("throws error on Future #2a");
}
), onError: (o) => Future(
() {
print("捕获 Future #1a 的异常 !: ${o.toString()}");
print('future #2b (a new future)');
throw Exception("throws error on Future #2b");
}
)
).catchError((e) {
print("catchError on chain : ${e.toString()}");
});
跑一跑上面的代码看个究竟:
I/flutter (29395): future #1a (a new future)
I/flutter (29395): 捕获 Future #1a 的异常 !: Exception: throws error on Future #1a
I/flutter (29395): future #2b (a new future)
I/flutter (29395): catchError on chain : Exception: throws error on Future #2b
答案很显然,也就是我们上面说的,then() 方法中的 onError 参数,会捕获调用者 Future 的异常,而在调用链上的 catchError() 则会捕获整个调用链中抛出的异常。
那么问题来了,对于一个从 Future 中抛出的异常,既可以通过向 then() 中传入 onError 参数来捕获,又可以通过 catchError() 捕获,那么具体用哪种方式呢?
官方更推荐我们用 catchError() 捕获更好,一方面函数处理成功的回调与失败的回调分开会让程序更易读,另一方面,使用 catchError() 捕获异常,会使你编写异步代码就像同步代码一样轻松。
// 官方推荐的异常捕获方式
Future(() {
// 需要异步处理的函数
}).then((value) => {
// 异步处理成功的回调
}).catchError((e) => {
// 异步函数发生错误的回调
});
// 同步代码
try {
int value = foo();
return bar(value);
} catch (e) {
return 499;
}
// 异步代码
Future<int> asyncValue = Future(foo); // Result of foo() as a future.
asyncValue.then((int value) {
return bar(value);
}).catchError((e) {
return 499;
});
总结
现在你已经改理解了 Dart 的异步机制,这里对以上介绍的主要部分做个总结。
- Event Loop 中有两个任务队列,分别是 microtask queue 和 event queue
- microtask queue 的任务主要来源于 Dart 内部,而 event queue 的任务来源于所有的外部发生的事件,如 I/O,屏幕的点击、组件的绘画,timers,isolates之间的消息传递等等。
- microtask queue 任务队列的优先级比较高,event loop 会首先执行完 microtask queue 中的任务,然后才会执行 event queue 中的任务。
向任务队列添加任务时需要遵循的几个规则:
- 如果可以的话,将任务都添加到 event queue 中。(通过 new Future() 或 Future.delayed())
- 两个队列中任务的执行时间不宜过长,microtask queue 中任务执行时间过长的话,会导致 event queue 中的任务长时间得不到执行。event queue 中每个任务执行时间过长的话,也会导致应用的响应不及时、掉帧,降低用户体验。
兄dei,如果觉得我写的还不错,麻烦帮个忙呗 😃
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
- 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
- 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!
拜托拜托,谢谢各位同学!