Void-7's

Flutter入门:Async之事件循环

最近在b站找到一个很好的Flutter学习UP:王叔不秃,推荐给大家。这里记录一下

事件循环 Event Loop

我们在学习Flutter或者说正在入门的时候,一定在官方文档或各种培训视频里面看到过了“异步”以及“Async,“Await”之类的词语,但对于萌新来说常常对于概念模糊不清,且拥有其它面向对象语言编写经验的人也许会先入为主的把Flutter的异步编程和已有认知等同起来,这样就给后面的学习造成了很多障碍。这里我们来梳理一下Flutter中的Async相关概念

常见的Flutter异步编程有下面几种”语法糖“的形式(以延迟2秒后执行setState函数为例):

//第一种
void func() async{
    await Future.delayed(Duration(seconds:2));
    setState((){
        //任意语句
    })
}
//第二种,不用async/await组合
void func() {
    Future.delayed(Duration(seconds:2),(){
        setState((){
        //任意语句
        });
    });
}
//第三种,更加原始的,我们可以直接使用.then回调
void func() {
    Future.delayed(Duration(seconds:2).then(
        (value)=>setState((){
            //任意语句
        })
    );
}

说到这呢,很多人可能会觉得异步操作应该是多线程的吧,对于Java学习者也许是这样(需要new thread),但对于JS学习者来说,异步操作不是多线程的,毕竟JS是单线程的。可以说,Java和JS差距还是很大的,而Flutter在异步操作方面与Javascript十分接近。包括Future和Promise,async/await等等专有名词都是近似或者完全一样。

这里我们来明确几个概念:

  • 程序无响应

我们在使用和操作app的时候感受到的“卡顿”,大多数情况下是由于没有时间来更新UI来不及刷新屏幕,那么为什么这么忙呢?这可能是由于较大的计算量或者网络资源加载带来的等待,所以在多线程的机制中,解决思路就是每当我们遇到需要等的东西,我们就单独派一个进程去“等待”,这样负责UI渲染的主进程就不会挂起,用户自然不会再感觉到卡顿了。但是在Dart中,事情就不是这样简单了。因为Dart语言中,每个线程都是封装在Isolate中的,每一个Isolate之间是不共享内存的,通信需要互相发消息。这样的隔离是有很多好处的,因为无法共享变量,也就不会造成进程内的变量被其它进程修改。Racing, Dead Lock也就不复存在了。另外,由于每个线程十分独立,所以,垃圾回收机制也就做的更加高效了。

  • 事件循环和事件队列

回到事件循环的概念上。假设我们的App代码只有百行千行,那么Main方法进入后顺序执行,最多数秒时间就能够完成,那么程序也不可能说执行完就这样退出了。怎么办呢?这里就要用到“事件循环”这个概念了,那么它究竟在做些什么呢?

——主要就是在做一件事:监视Event Queue事件队列,在事件队列中如果发现了新的事件,那么就把相应的代码送过去执行,所以所谓的“异步操作”,就是在事件队列中添加事件来完成的。比如说服务器的请求一般就会先产生一个Future对象(类似于JS的Promise),我们可以用Future.then的方法告诉程序,我们需要在Future完成的时候执行什么代码,在等待Future还没完成的时候,我们就可以做别的事情了。比如说,我们在等待服务器的回应,在此期间,用户可以点击别的按钮,程序先处理用户点击的事件,这样就不会有卡顿的感觉了。除了服务器的请求会得到一个Future之外,我们也可以手动向事件队列中添加事件,这可能需要借助Future的构造函数,或者delayed函数,比如:

void main(){
    Future(
        ()=>print('A')
    );
    print('B');
}

其运行结果是BA而不是AB,这就是因为程序先把Future构造函数的回调(打印A)的事件放到事件队列中了,然后打印了B,这时候main方法已经完成,那么去事件队列中查看是否有事件需要执行:发现有一个打印A的事件,就执行它。

  • 微任务队列

除了Event Queue, Flutter还有一个Microtask Queue或者说微任务队列。它的优先级是比事件队列要高的,只要微任务队列中的非空那么我们就会先执行微任务队列中的事件,只有它是空的时候才去执行事件队列中的事件。同样的,我们也可以用scheduleMicrotask()函数向为任务队列中添加事件,但要注意尽量不要这么做。

image-20210307212011252.png

image-20210307212113252.png

当然,这样的单进程机制只能解决等待(数据库读取,用户输入等)的问题,如果是因为计算量大,那确实难以靠这样的机制来解决。这时候我们只能寻求Isolate的帮助,来创建真正的多进程了。

image-20210307212338799.png

这里列出了三种不同的运行情况:

第一列是会被直接运行的语句,sync同步执行与async异步执行相对,value也是一样的情况,传入的操作会被立刻执行。而_.then()方法是说在Future完成的时刻直接去执行传入的操作。除此之外的大多数语句也都是直接运行。

第二类是优先级高的任务(微任务),通常是那些比较能快速完成的任务,shceduleMicrotask()方法它可以将一个回调函数添加到微任务队列中。另外Future.microtask()方法内也会帮助我们调用scheduleMicrotask()方法。除了这两种情况,Future的then函数偶尔会被添加为Microtask,我们刚讲过,then()是在Future完成的瞬间立即被执行的(比如一个延迟五秒的Future结束时then中操作就会被推进EventQueue里,等到检查EventQueue的时候自然就立即执行这个操作),这里说的例外情况是:如果对一个已经完成的Future使用then方法,那么由于其已经完成,then中代码不会再被推进EventQueue了,所以需要尽快执行then中的代码的话,就将其推入MicrotaskQueue。

最后,最常见的是Future()方法,可以将事件直接添加到EventQueue,而delayed()方法则是延迟添加到事件队列。