作者:京东物流王志明1Dart中的事件循环模型 在App开发中,经常会遇到处理异步任务的场景,如网络请求、读写文件等。Android、iOS使用的是多线程,而在Flutter中为单线程事件循环,如下图所示 Dart中有两个任务队列,分别为microtask队列和event队列,队列中的任务按照先进先出的顺序执行,而microtask队列的执行优先级高于event队列。在main方法执行完毕后,会启动事件循环,首先将microtask队列中的任务逐个执行完毕,再去执行event队列中的任务,每一个event队列中的任务在执行完成后,会再去优先执行microtask队列中的任务,如此反复,直到清空所有队列,这个过程就是Dart事件循环的处理机制。这种机制可以让我们更简单的处理异步任务,不用担心锁的问题。我们可以很容易的预测任务执行的顺序,但无法准确的预测到事件循环何时会处理到你期望执行的任务。例如创建了一个延时任务,但排在前面的任务结束前是不会处理这个延时任务的,也就说这个任务的等待时间可能会大于指定的延迟时间。 Dart中的方法一旦开始执行就不会被打断,而event队列中的事件还来自于用户输入、IO、定时器、绘制等,这意味着在两个队列中都不适合执行计算量过大的任务,才能保证流畅的UI绘制和用户事件的快速响应。而且当一个任务的代码发生异常时,只会打断当前任务,后续任务不受影响,程序更不会退出。从上图还可以看出,将一个任务加入microtask队列,可以提高任务优先级,但是一般不建议这么做,除非比较紧急的任务并且计算量不大,因为UI绘制和处理用户事件是在event事件队列中的,滥用microtask队列可能会影响用户体验。 总结下Dart事件循环的主要概念:Dart中有两个队列来执行任务:microtask队列和event队列。事件循环在main方法执行完毕后启动,microtask队列中的任务会被优先处理。microtask队列只处理来自Dart内部的任务,event队列中有来自Dart内部的Future、Timer、isolatemessage,还有来自系统的用户输入、IO、UI绘制等外部事件任务。Dart中的方法执行不会被打断,因此两个队列中都不适合用来执行计算量大的任务。一个任务中未被处理的异常只会打断当前任务,后续任务不受影响,程序更不会退出。1。1向microtask队列中添加任务 可以使用顶层方法scheduleMicrotask或者Future。microtask方法,如下所示:scheduleMicrotask(()print(microtask1));Future。microtask(()print(microtask2)); 使用Future。microtask的优势在于可以在then回调中处理任务返回的结果。1。2向event队列中添加任务Future(()print(eventtask)); 基于以上理论,通过如下代码可以验证Dart的事件循环机制:voidmain(){print(mainstart);Future(()print(eventtask1));Future。microtask(()print(microtask1));Future(()print(eventtask1));Future。microtask(()print(microtask2));print(mainstop); 执行结果:mainstartmainstopmicrotask1microtask2eventtask1eventtask1 通过输出结果可以看到,任务的执行顺序并不是按照编写代码的顺序来的,将任务添加到队列不会立刻执行,而执行顺序也完全符合前面讲的规则,当前main方法中的代码执行完毕后,才会去执行队列中的任务,且microTask队列的优先级高于event队列。2Dart中的异步实现 在Dart中通过Future来执行异步任务,Future是对异步任务状态的封装,对任务结果的代理,通过then方法可以注册处理任务结果的回调方法。 创建方法Future方式: Future() Future。delayed() Future。microtask() Future。sync()2。1Future()factoryFuture(FutureOrTcomputation()){FutureTresultnewFutureT();Timer。run((){try{result。complete(computation());}catch(e,s){completeWithErrorCallback(result,e,s);}});returnresult;} 上面是Future()的源码,可以看到内部是通过启动一个没有延迟的计时器来添加任务的,实用trycatch来捕获任务代码中可能出现的异常,我们可以在catchError回调中来处理异常。2。2Future。delayed()factoryFuture。delayed(Durationduration,〔FutureOrTcomputation()?〕){if(computationnull!typeAcceptsNullT()){throwArgumentError。value(null,computation,Thetypeparameterisnotnullable);}FutureTresultnewFutureT();newTimer(duration,(){if(computationnull){result。complete(nullasT);}else{try{result。complete(computation());}catch(e,s){completeWithErrorCallback(result,e,s);}}});returnresult;} Future。delayed()与Future()的区别是通过一个延迟的计时器来添加任务。2。3Future。microtask()factoryFuture。microtask(FutureOrTcomputation()){FutureTresultnewFutureT();scheduleMicrotask((){try{result。complete(computation());}catch(e,s){completeWithErrorCallback(result,e,s);}});returnresult;} Future。microtask()是将任务添加到microtask队列,通过这种可以很方便通过then方法中的回调来处理任务的结果。2。4Future。sync()factoryFuture。sync(FutureOrTcomputation()){try{varresultcomputation();if(resultisFutureT){returnresult;}else{TODO(40014):Removecastwhentypepromotionworks。returnnewFutureT。value(resultasdynamic);}}catch(error,stackTrace){varfuturenewFutureT();AsyncError?replacementZone。current。errorCallback(error,stackTrace);if(replacement!null){future。asyncCompleteError(replacement。error,replacement。stackTrace);}else{future。asyncCompleteError(error,stackTrace);}returnfuture;}} Future。sync()中的任务会被立即执行,不会添加到任何队列。 在第一个章节中讲到了可以很容易的预测任务的执行顺序,下面我们通过一个例子来验证:voidmain(){print(mainstart);Future。microtask(()print(microtask1));Future。delayed(newDuration(seconds:1),()print(delayedevent));Future(()print(event1));Future(()print(event2));Future。microtask(()print(microtask2));print(mainstop);} 执行结果:mainstartmainstopmicrotask1microtask2event1event2delayedevent 因为代码比较简单,通过代码可以很容易的预测到执行结果,下面将复杂度稍微提高。voidmain(){print(mainstart);Future。microtask(()print(microtask1));Future。delayed(newDuration(seconds:1),()print(delayedevent));Future(()print(event1))。then(()print(event1callback1))。then(()print(event1callback2));Future(()print(event2))。then((){print(event2callback1);returnFuture(()print(event4))。then(()print(event4callback));})。then((){print(event2callback2);Future(()print(event5))。then(()print(event5callback));})。then((){print(event2callback3);Future。microtask(()print(microtask3));})。then((){print(event2callback4);});Future(()print(event3));Future。sync(()print(synctask));Future。microtask(()print(microtask2))。then(()print(microtask2callbak));print(mainstop);} 执行结果:mainstartsynctaskmainstopmicrotask1microtask2microtask2callbakevent1event1callback1event1callback2event2event2callback1event3event4event4callbackevent2callback2event2callback3event2callback4microtask3event5event5callbackdelayedevent 看到结果后你可能会疑惑,为什么event1、event1callback1、event1callback2会连续输出,而event2callback1输出后为什么是event3,event5、event5callback为什么会在microtask3后输出? 这里我们补充下then方法的一些关键知识,理解了这些,上面的输出结果也就很好理解了:then方法中的回调并不是按照它们注册的顺序来执行。Future中的任务执行完毕后会立刻执行then方法中的回调,并且回调不会被添加到任何队列中。如果Future中的任务在then方法调用之前已经执行完毕了,那么会有一个任务被加入到microtask队列中。这个任务执行的就是被传入then方法中的回调。2。5catchError、whenCompleteFuture((){throwerror;})。then((){print(success);})。catchError((error){print(error);})。whenComplete((){print(completed);}); 输出结果:errorcompleted 通过catchError方法注册的回调,可以用来处理任务代码产生的异常。不管Future中的任务执行成功与否,whenComplete方法都会被调用。2。6async、await 使用async、await能以更简洁的编写异步代码,是Dart提供的一个语法糖。使用async关键字修饰的方法返回值类型为Future,在async方法内可以使用await关键字来修饰异步任务,在方法内部达到同步执行的效果,可以达到简化代码和提高可读性的效果,不过如果想要处理异常,需要实用trycatch语句来包裹await修饰的异步任务。voidmain()async{print(awaitgetData());}FutureintgetData()async{finalaawaitFuture。delayed(Duration(seconds:1),()1);finalbawaitFuture。delayed(Duration(seconds:1),()1);returnab;}3Isolate介绍 前面讲到耗时任务不适合放到microtask队列或event队列中执行,会导致UI卡顿。那么在Flutter中有没有既可以执行耗时任务又不影响UI绘制呢,其实是有的,前面提到microtask队列和event队列是在mainisolate中运行的,而isolate是在线程中运行的,那我们开启一个新的isolate就可以了,相当于开启一个新的线程,使用多线程的方式来执行任务,Flutter也为我们提供了相应的Api。3。1computevoidmain()async{computeString,String(getData,Alex,)。then((result){print(result);});}StringgetData(Stringname){模拟耗时3秒sleep(Duration(seconds:3));returnHelloname;} compute第一个参数是要执行的任务,第二个参数是要向任务发送的消息,需要注意的是第一个参数只支持顶层参数。使用compute()可以方便的执行耗时任务,但是滥用的话也会适得其反,因为每次调用,相当于新建一个isolate。上面的代码执行一个经历了isolate的创建以及销毁过程,还有数据的传递会经历两次拷贝,因为isolate之间是完全隔离的,不能共享内存,整个过程除去任务本身的执行时间,也会非常的耗时,isolate的创建也比较消耗内存,创建过多的isolate还有OOM的风险。这时我们就需要一个更优的解决方案,减少频繁创建销毁isolate所带来的消耗,最好是能创建一个类似于线程池的东西,只要提前初始化好,后面就可以随时使用,不用担心会发生前面所讲的问题,这时候LoadBalancer就派上用场了3。2LoadBalancer用来创建LoadBalancerFutureLoadBalancerloadBalancerCreatorLoadBalancer。create(2,IsolateRunner。spawn);全局可用的loadBalancerlateLoadBalancerloadBalancer;voidmain()async{初始化LoadBalancerloadBalancerawaitloadBalancerCreator;使用LoadBalancer执行任务finalresultawaitloadBalancer。runString,String(getData,Alex);print(result);}StringgetData(Stringname){模拟耗时3秒sleep(Duration(seconds:3));returnHelloname;} 使用LoadBalancer。create()方法可以创建出一个isolate线程池,能够指定isolate的数量,并自动实现了负载均衡。应用启动后在合适的时机将其初始化好,后续就有一个全局可用的LoadBalancer了。4实用经验4。1指定任务的执行顺序 在开发中经常会有需要连续执行异步任务的场景,例如下面的例子,后面的一步任务直接需要以来前面任务的结果,所有任务正常执行完毕才算成功。voidmain()async{print(awaitgetData());}FutureintgetData(){finalcompleterCompleterint();intvalue0;Future((){return1;})。then((result1){valueresult1;returnFuture((){return2;})。then((result2){valueresult2;returnFuture((){return3;})。then((result3){valueresult3;completer。complete(value);});});});returncompleter。future;} 这种方式出现了回调地狱,代码非常难以阅读,实际开发中还会有处理异常的代码,会显得更加臃肿,编写难度也大,显然这种方式是不建议使用的。4。2使用then的链式调用voidmain()async{print(awaitgetData());}FutureintgetData(){intvalue0;returnFuture(()1)。then((result1){valueresult1;returnFuture(()2);})。then((result2){valueresult2;returnFuture(()3);})。then((result3){valueresult3;returnvalue;});} 回调地狱的问题解决了,代码可读性提高很多。4。3使用async、awaitvoidmain()async{print(awaitgetData());}FutureintgetData()async{intvalue0;valueawaitFuture(()1);valueawaitFuture(()2);valueawaitFuture(()3);returnvalue;} 效果显而易见,代码更加清晰了。4。4取消任务 在前面讲到了Dart方法执行时是不能被中断的,这就意味着一个Future任务开始后必然会走到完成的状态,但是很多时候我们需要又取消一个异步任务,唯一的办法就是在任务结束后不执行回调代码,就可以实现类似取消的效果。4。5CancelableOperation 在Flutter的async包中,提供了一个CancelableOperation给我们使用,使用它可以很简单的实现取消任务的需求。voidmain()async{创建一个可以取消的任务finalcancelableOperationCancelableOperation。fromFuture(Future(()async{print(start);awaitFuture。delayed(Duration(seconds:3));模拟耗时3秒print(end);}),onCancel:()print(cancel。。。),);注册任务结束后的回调cancelableOperation。value。then((val){print(finished);});模拟1秒后取消任务Future。delayed(Duration(seconds:1))。then(()cancelableOperation。cancel());} CancelableOperation是对Future的代理,对Future的then进行了接管,判断isCanceled标记决定是否需要执行用户提供的回调。