Flutter 的线程模型和异步原理

d29dd39c076fe6473a21b2e02fe4df61.gif

本文字数::36130

预计阅读时间:91分钟

Android应用中, 用户时常会遇到界面卡顿的情况,非常影响用户的体验。作为Android开发肯定都知道:应用在主线程里做了大量的耗时操作(例如文件读写, 数据库读写,网络访问,图片编解码等),就会导致UI不能及时刷新,出现跳帧现象。如果应用主线程不能及时处理用户的输入事件或广播消息,系统甚至会直接弹出著名的ANR对话框,提示用户杀死应用。

Flutter应用中,如果出现界面卡顿,它的原因也是如此吗?

我们带着这些疑问,一起来搞清楚Flutter的线程模型和异步原理,并找到问题的答案。

一、Flutter系统结

首先,我们熟悉下Flutter官方提供的系统结构图:

df4d2548f132c1f47158d4c1a0d86cc7.jpeg

整体框架是采用分层设计的,自上而下分别是:FrameworkEngineEmbedder

  • Framework:基于Dart语言构建的Framework,包括了各种UI组件,动画和手势识别等,并将所有的设计通过Widgets小组件层进行抽象封装。所以在Flutter中一切都是 Widget

  • Engine:基于C/C++构建的引擎,包括了SkiaDartText等,实现了Flutter渲染引擎,文字排版,事件处理和Dart运行时等功能。SkiaText 为上层提供调用底层渲染和排版的功能,Dart运行时提供了调用Dart和渲染引擎的能力;

  • Embedder:嵌入层是操作系统适配层,会将Flutter嵌入到各个平台上。嵌入层负责适配原生平台插件、线程管理、渲染Surface设置等。

从架构图中可以看到,Embedder负责线程的创建和管理,并且提供Task RunnerEngine使用。Engine虽然自己并不创建和管理线程,但是它通过 Dart VM(Dart Runtime Mgmt)提供IsolateFramework和应用层进行多线程创建。

二、Task Runner

接下来,我们继续了解Embeder提供的四个Task RunnerPlatform RunnerUI RunnerGPU RunnerIO Runner

Flutter的代码基本上由这四个Runner负责运行,每个Runner负责不同的任务, 不只处理Engine的任务,还处理Native Plugin带来的原生平台任务。

Android中,每个Flutter Engine各自拥有一个UI RunnerGPU RunnerIO Runner,但是一个应用中的所有Engine共享一个Platform Runner。每个Runner都是一个平台线程,且Engine会将UI RunnerRoot Isolate 进行互相绑定。但RunnerIsolate本身是相互独立的,IsolateDart VM 进行管理,不由Runner管理。

  • Platform Runner

Platform Runner运行在平台的Main Thread,负责执行Flutter Engine的代码和Native Plugin任务。

如果在Platform Runner中运行耗时任务,会影响原生平台任务的正常执行。但是Platform Runner被阻塞后并不会导致页面卡顿。因为Platform Runner不负责Flutter的页面渲染,这点和Android原生应用不一样。

  • UI Runner

UI Runner负责为Engine执行Root Isolate的代码,而Root Isolate负责运行所有Dart代码。

Root Isolate绑定了很多UI Runner的处理函数,负责创建管理Layer Tree最终绘制到屏幕上的内容,因此这个线程被阻塞会直接导致界面卡顿掉帧。

每当页面更新的vsync到来时,Root Isolate会对Widgets进行layout,生成Layer tree等页面显示信息,提交给Engine去处理。

所以,在Root Isolate中运行耗时任务会导致页面显示卡顿。

  • GPU Runner

GPU Runner负责将UI Runner提供的Layer Tree信息转化为平台可执行的GPU指令,并提交给渲染平台,如Skia

GPU Runner还负责管理绘制所需要的GPU资源,比如平台FramebufferSurfaceTextureBuffers等。

GPU Runner相对比较独立,除了EmbedderRunner线程外,其他线程均不可向其提交渲染信息。

  • IO Runner

IO Runner负责将读取的图片解压转换成GPU能够处理的格式并提交给GPU Runner处理。

Image这样的资源通过async call调用时,Framework会通知IO Runner进行图片的异步加载,进行格式处理,然后通过GPU RunnerContext引用,提交给GPU Runner处理。

由上可知,在Android  Flutter应用中,如果出现界面卡顿,它的原因和Android应用的原因并不相同。Flutter应用中平台线程的阻塞不会影响界面的卡顿,而UI Runner的阻塞必然导致页面卡顿。

那么,在Flutter应用中,像网络请求,文件读取,海量计算,图片处理,编解码等耗时任务都应该怎么处理,才能不阻塞UI Runner呢?

要解释清楚这个问题,我们需要先了解Flutter中的线程模型:Isolate

三、线程模型

IsolateDart平台对线程的实现方案,所以和线程一样,也可以利用多核CPU去处理大量耗时任务。

Isolate底层实际还是使用操作系统提供的OSThread。但和普通Thread 不同,Isolate拥有独立的内存,由线程加独立内存构成。

由于Isolate线程之间内存不共享,所以Isolate线程之间并不存在共享数据的问题,所以也不需要Isolate数据同步机制。

Isolate之间虽然不能共享数据,但是可以通过端口 Port 的方式进行数据通信。

3.1 Isolate

Android Flutter应用启动后,会首先执行main函数,接着调用runApp,然后创建一个Flutter EnginEngin会启动四个Task Runner,并且UI Runner开始执行Root Isolate主线程中的代码。

Flutter应用默认在单线程中运行Dart代码,如果不开启新的线程,所有Dart 代码都在Root Isolate主线程中运行。

我们可以通过Isolate的创建和Port数据通信的例子来进一步了解代码执行的线程情况:

// kiki_main_tab_page.dart
class KKMainTabPageState extends BaseThemeState<KKMainTabPage>with WidgetsBindingObserver {// 这是摸鱼kik的主界面......@overridevoid initState() {super.initState();......//测试 Future 中代码的运行所属 IsolateFuture.delayed(const Duration(milliseconds: 500)).then((value) {KKMethodChannelUtil.setAndroidStatusBarTheme(AppScreenMedia.isLight);print_cjf('initState 1---' + Isolate.current.debugName.toString());});print_cjf('initState 2---' + Isolate.current.debugName.toString());//测试在新的 isolate 中请求数据。此处没有用await等待方法的异步结果。testIsolate(); print_cjf('initState 3---' + Isolate.current.debugName.toString());}@overrideWidget build(BuildContext context) {print_cjf('build 4---' + Isolate.current.debugName.toString());......}......
}// test_isolate.dart
void testIsolate() async {print_cjf('testIsolate start---' + Isolate.current.debugName.toString());//创建 ReceivePort,用来接受新 Isolate 发送的消息ReceivePort receivePort1 = ReceivePort();print_cjf('testIsolate 1---' + Isolate.current.debugName.toString());//创建新的 Isolate,并且把 receivePort1 的发送端口传给 newIsolatevar newIsolate = await Isolate.spawn(dataLoader, receivePort1.sendPort);print_cjf('testIsolate 2---' + Isolate.current.debugName.toString());//等待 newIsolate 发送的异步消息(异步消息只有 newIsolate 的发送端口 receivePort3.sendPort)。//通过 first 获得异步消息后,会立即关闭 receivePort1 的 sendPort 端口。SendPort sendPort3 = await receivePort1.first;print_cjf('testIsolate 3---' + Isolate.current.debugName.toString());//因为上面 first 函数获取异步消息后,关闭了 receivePort1 的 sendPort 端口//所以要新创建一个新的 ReceivePort 接受 newIsolate 发送过来的消息ReceivePort receivePort2 = ReceivePort();print_cjf('testIsolate 4---' + Isolate.current.debugName.toString());//使用 newIsolate 的发送端口 sendPort3,发送异步消息(异步消息包括网络 url,和 rootIsolate 的 receivePort2 发送端口)sendPort3.send(['https://jsonplaceholder.typicode.com/posts/1',receivePort2.sendPort]);print_cjf('testIsolate 5.1---' + Isolate.current.debugName.toString());sendPort3.send(["https://w.sohu.com/detail/1",receivePort2.sendPort]);print_cjf('testIsolate 5.2---' + Isolate.current.debugName.toString());sendPort3.send(["aaaaaa",receivePort2.sendPort]);print_cjf('testIsolate 5.3---' + Isolate.current.debugName.toString());sendPort3.send(["bbbbbb",receivePort2.sendPort]);print_cjf('testIsolate 5.4---' + Isolate.current.debugName.toString());//获取 newIsolate 发送来的异步消息(异步获取的网络数据)//方法一: 不等待 循环获取异步数据receivePort2.listen((msg) {print_cjf('testIsolate 6.2---' + Isolate.current.debugName.toString() + '---$msg');});print_cjf('testIsolate end---' + Isolate.current.debugName.toString());
}void dataLoader(SendPort sendPort1) async {print_cjf('dataLoader start---' + Isolate.current.debugName.toString());ReceivePort receivePort3 = ReceivePort();sendPort1.send(receivePort3.sendPort);print_cjf('dataLoader 1---' + Isolate.current.debugName.toString());await for (var msg in receivePort3) {String dataUrl = msg[0];SendPort sendPort2 = msg[1];// 暂不从网络中获取数据,直接模拟一个数据返回sendPort2.send('dataLoader 2.1---' + Isolate.current.debugName.toString() + "---$dataUrl");print_cjf('dataLoader 3.1---' + Isolate.current.debugName.toString() + "---$dataUrl");if(dataUrl.startsWith("aaaaaa")) {//销毁当前 isolate, 并发送结束消息给 sendPort2Isolate.exit(sendPort2, "dataLoader 4 ---Isolate exit with last message to sendPort2");}}print_cjf('dataLoader end---' + Isolate.current.debugName.toString()); //不会被执行到。
}// 输出 log 
I/flutter ( 4910): 11:25:56:587:cjf---initState 2---main //KKMainTabPageState 的 initState, 开始调用
I/flutter ( 4910): 11:25:56:588:cjf---testIsolate start---main 
I/flutter ( 4910): 11:25:56:589:cjf---testIsolate 1---main //调用 await Isolate.spawn 创建新 isolate,testIsolate 方法中断执行,需等待异步结果回来后恢复执行。
I/flutter ( 4910): 11:25:56:590:cjf---initState 3---main //KKMainTabPageState 的 initState,await Isolate.spawn 会进行异步调度,所以会返回到 initState 中继续执行
I/flutter ( 4910): 11:25:56:591:cjf---build 4---main //KKMainTabPageState 的 build,后续还会被多次调用I/flutter ( 4910): 11:25:56:592:cjf---testIsolate 2---main //异步执行,await Isolate.spawn 的异步结果返回,testIsolate 后续代码继续执行
I/flutter ( 4910): 11:25:57:086:cjf---initState 1---main //异步执行,Future.delayed 时间到了后,异步任务会得到执行,且运行在 Root Isolate 中I/flutter ( 4910): 11:25:57:133:cjf---dataLoader start---dataLoader//新建 isolate 内部, 新的 isolate 开始运行
I/flutter ( 4910): 11:25:57:134:cjf---dataLoader 1---dataLoader //新建 isolate 内部, 将自己的 SendPort3 发送给 RootIsolate 的 SendPort1I/flutter ( 4910): 11:25:57:140:cjf---testIsolate 3---main //异步执行,await receivePort1.first,仍然在 root isolate 中
I/flutter ( 4910): 11:25:57:141:cjf---testIsolate 4---mainI/flutter ( 4910): 11:25:57:142:cjf---testIsolate 5.1---main //给 SendPort3 发送 jsonplaceholder.typicode.com 数据和 sendPort2
I/flutter ( 4910): 11:25:57:144:cjf---testIsolate 5.2---main //给 SendPort3 发送 w.sohu.com 数据和 sendPort2
I/flutter ( 4910): 11:25:57:144:cjf---dataLoader 3.1---dataLoader---https://jsonplaceholder.typicode.com/posts/1
I/flutter ( 4910): 11:25:57:144:cjf---testIsolate 5.3---main //给 SendPort3 发送 aaaaaa 数据和 sendPort2
I/flutter ( 4910): 11:25:57:145:cjf---dataLoader 3.1---dataLoader---https://w.sohu.com/detail/1
I/flutter ( 4910): 11:25:57:145:cjf---testIsolate 5.4---main //给 SendPort3 发送 bbbbbb 数据和 sendPort2
I/flutter ( 4910): 11:25:57:146:cjf---testIsolate end---main //设置 listen 数据,退出 testIsolate 方法
I/flutter ( 4910): 11:25:57:146:cjf---dataLoader 3.1---dataLoader---aaaaaaI/flutter ( 4910): 11:25:57:148:cjf---testIsolate 6.2---main---dataLoader 2.1---dataLoader---https://jsonplaceholder.typicode.com/posts/1
I/flutter ( 4910): 11:25:57:149:cjf---testIsolate 6.2---main---dataLoader 2.1---dataLoader---https://w.sohu.com/detail/1
I/flutter ( 4910): 11:25:57:149:cjf---testIsolate 6.2---main---dataLoader 2.1---dataLoader---aaaaaa
I/flutter ( 4910): 11:25:57:150:cjf---testIsolate 6.2---main---dataLoader 4 ---Isolate exit with last message to sendPort2I/flutter ( 4910): 11:25:57:588:cjf---build 4---main //KKMainTabPageState 的 build,被多次调用

可以从上述例子,Log和注释可以看到:

  • Root IsolatedebugName也是"main",而不是"root"

  • 通过Isolate.spawn创建新的Isolate,debugName即为方法名"dataLoader"

  • 除了新建Isolate中的代码,其它Dart代码都默认运行在Root Isolate中,包括widget中的代码,Future中的代码,和await异步方法中的代码。如:"initState 3---main""build 4---main","initState 1---main""testIsolate 1---main" 都在"main"线程中运行;

  • await方法调用,需要等待异步任务结果,会停止当前方法后续代码的执行,但不会阻塞调用者后续代码的执行,如log "initState 3" 的输出并没有等待log "testIsolate 2"的输出。包括widget的构建log "build 4", 和initState中的Future delay异步任务的log "initState 1"都得到了执行,而不是一直阻塞等待testIsolate方法中的await Isolate.spawnawait receivePort1.first语句的返回;

  • Isolate之间通过ReceivePort进行信息发送和接送,且消息传递的过程是异步的。如,log "testIsolate 5.1""testIsolate 5.2""testIsolate 5.3""testIsolate 5.4"log "dataLoader 3.1"是穿插着打印出来的;

  • Isolate之间可以通过await receivePort1.first来阻塞式接收一次消息,或通过receivePort2.listen来非阻塞式循环接收信息,或通过await for receivePort3来阻塞式循环接收信息。

从例子中我们不但可以看到Isolate的创建和Port数据通信方式,同时也看到了Dart代码都默认运行在 Root Isolate 主线程中。   

  3.2 compute

上面例子中创建一个Isolate进行数据通讯,步骤较为麻烦。为此Flutter为我们提供了一个简版的Isolatecompute

compute很适合简单,高CPU的耗时任务处理,它可以很便捷地进行多线程任务开发,我们举例进行说明:

// test_isolate.dart
void testCompute() async {print_cjf('testCompute start---' + Isolate.current.debugName.toString());Future.delayed(new Duration(seconds: 1),(){print_cjf('testCompute 1---' + Isolate.current.debugName.toString());});var count = await compute(countTask, 1234567890);print_cjf('testCompute end---' + Isolate.current.debugName.toString()+ "---count=" + count.toString());
}int countTask(int num1) {print_cjf('countTask start ---' + Isolate.current.debugName.toString());int count = 0;while (num1 > 0) {if (num1 % 2 == 0) { count++; }num1--;}print_cjf('countTask end---' + Isolate.current.debugName.toString());return count;
}// 输出 Log
I/flutter ( 4910): 12:44:32:731:cjf---testCompute start---main
I/flutter ( 4910): 12:44:33:254:cjf---countTask start ---Closure: (int) => int from Function 'countTask': static.
I/flutter ( 4910): 12:44:33:736:cjf---testCompute 1---main
I/flutter ( 4910): 12:45:16:329:cjf---countTask end---Closure: (int) => int from Function 'countTask': static.
I/flutter ( 4910): 12:45:16:330:cjf---testCompute end---main---count=617283945

compute方法是Flutter foundation包中的顶层方法,它对Isolate.spawnsendPort消息发送做了封装,返回一个异步结Future<R>

通过封装后的接口,我们只需简单调用await compute,就可以等待一个耗时 43s 的计算任务的异步执行结果,并且等待过程中没有阻塞Root Isolate中其它异步任务地执行,比如,通过Future.delayed插入的异步任务"testcompute 1---main" ,在countTask Isolate执行的同时,在Root Isolate中被执行了。  

  3.3 Memory

上面 2 个例子,要么是直接等待新Isolate的异步结果,要么是通过port进行数据传输,并没有使用全局数据进行共享,所有也没有数据同步问题。

实际上在Flutter中确实没有类似Android的全局共享数据,因为Flutter中的Isolate拥有独立的内存,数据没法共享,只能通过port传输。

我们继续通过一个简单的例子,来说明Isolate独立内存这个特点:

// test_isolate.dart
int intValue = 0; //定义普通 int 顶层变量
IntObject intObject = IntObject(); //定义对象 IntObject 顶层变量class IntObject {int _i = 0;void increase() { _i++; }int get() { return _i;}
}void testIsolateMemory() async {print_cjf('testIsolateMemory start');final receive = ReceivePort();receive.listen((msg) {//打印 MemoryTask Isolate 传过来的Stringprint_cjf('testIsolateMemory ---'+ "data===$msg");//打印 Root Isolate 中的变量值print_cjf('testIsolateMemory ---'+ "i=$intValue, intObject=${intObject.get()}");});//5s后,给顶层变量加1Future.delayed(const Duration(seconds: 5),(){intValue++;intObject.increase();print_cjf('testIsolateMemory ---'+ "delayed:i=$intValue, intObject=${intObject.get()}");});Isolate isolate = await Isolate.spawn(MemoryTask, receive.sendPort);print_cjf('testIsolateMemory end');
}void MemoryTask(SendPort sendPort) {int counter = 0; //MemoryTask Isolate 中的局部变量print_cjf('MemoryTask start');//每隔 1s,给顶层变量和局部变量都加1Timer.periodic(const Duration(seconds: 1), (_) {counter++;intValue++;intObject.increase();String sendMsg = "counter=$counter, i=$intValue, intObject=${intObject.get()}";//打印 MemoryTask Isolate 中的变量值print_cjf('MemoryTask ---$sendMsg');sendPort.send(sendMsg);if(counter >= 10) {//销毁当前 isolate, 并发送结束消息给 sendPortIsolate.exit(sendPort, "MemoryTask ---Isolate exit with last message to sendPort");}});print_cjf('MemoryTask end');
}// 输出 Log
I/flutter ( 4910): 12:58:21:555:cjf---testIsolateMemory start
I/flutter ( 4910): 12:58:21:575:cjf---testIsolateMemory end
I/flutter ( 4910): 12:58:22:84:cjf---MemoryTask start
I/flutter ( 4910): 12:58:22:86:cjf---MemoryTask endI/flutter ( 4910): 12:58:23:93:cjf---MemoryTask ---counter=1, i=1, intObject=1//MemoryTask Isolate 中的顶层
变量发生改变
I/flutter ( 4910): 12:58:23:96:cjf---testIsolateMemory ---data===counter=1, i=1, intObject=1
I/flutter ( 4910): 12:58:23:97:cjf---testIsolateMemory ---i=0, intObject=0 //Root Isolate 中的顶变量没有改变
I/flutter ( 4910): 12:58:24:87:cjf---MemoryTask ---counter=2, i=2, intObject=2
I/flutter ( 4910): 12:58:24:89:cjf---testIsolateMemory ---data===counter=2, i=2, intObject=2
I/flutter ( 4910): 12:58:24:90:cjf---testIsolateMemory ---i=0, intObject=0 //Root Isolate 中的顶变量没有改变
I/flutter ( 4910): 12:58:25:89:cjf---MemoryTask ---counter=3, i=3, intObject=3
I/flutter ( 4910): 12:58:25:90:cjf---testIsolateMemory ---data===counter=3, i=3, intObject=3
I/flutter ( 4910): 12:58:25:91:cjf---testIsolateMemory ---i=0, intObject=0 //Root Isolate 中的顶变量没有改变
I/flutter ( 4910): 12:58:26:89:cjf---MemoryTask ---counter=4, i=4, intObject=4
I/flutter ( 4910): 12:58:26:90:cjf---testIsolateMemory ---data===counter=4, i=4, intObject=4
I/flutter ( 4910): 12:58:26:91:cjf---testIsolateMemory ---i=0, intObject=0 //Root Isolate 中的顶变量没有改变I/flutter ( 4910): 12:58:26:621:cjf---testIsolateMemory ---delayed:i=1, intObject=1 //Root Isolate 中给顶层变量加1I/flutter ( 4910): 12:58:27:88:cjf---MemoryTask ---counter=5, i=5, intObject=5
I/flutter ( 4910): 12:58:27:90:cjf---testIsolateMemory ---data===counter=5, i=5, intObject=5
I/flutter ( 4910): 12:58:27:90:cjf---testIsolateMemory ---i=1, intObject=1 //Root Isolate 中的顶层变量发生改变I/flutter ( 4910): 12:58:28:89:cjf---MemoryTask ---counter=6, i=6, intObject=6
I/flutter ( 4910): 12:58:28:90:cjf---testIsolateMemory ---data===counter=6, i=6, intObject=6
I/flutter ( 4910): 12:58:28:91:cjf---testIsolateMemory ---i=1, intObject=1
I/flutter ( 4910): 12:58:29:87:cjf---MemoryTask ---counter=7, i=7, intObject=7
I/flutter ( 4910): 12:58:29:89:cjf---testIsolateMemory ---data===counter=7, i=7, intObject=7
I/flutter ( 4910): 12:58:29:90:cjf---testIsolateMemory ---i=1, intObject=1
I/flutter ( 4910): 12:58:30:89:cjf---MemoryTask ---counter=8, i=8, intObject=8
I/flutter ( 4910): 12:58:30:91:cjf---testIsolateMemory ---data===counter=8, i=8, intObject=8
I/flutter ( 4910): 12:58:30:92:cjf---testIsolateMemory ---i=1, intObject=1
I/flutter ( 4910): 12:58:31:89:cjf---MemoryTask ---counter=9, i=9, intObject=9
I/flutter ( 4910): 12:58:31:91:cjf---testIsolateMemory ---data===counter=9, i=9, intObject=9
I/flutter ( 4910): 12:58:31:91:cjf---testIsolateMemory ---i=1, intObject=1
I/flutter ( 4910): 12:58:32:89:cjf---MemoryTask ---counter=10, i=10, intObject=10
I/flutter ( 4910): 12:58:32:90:cjf---testIsolateMemory ---data===counter=10, i=10, intObject=10
I/flutter ( 4910): 12:58:32:91:cjf---testIsolateMemory ---i=1, intObject=1I/flutter ( 4910): 12:58:32:91:cjf---testIsolateMemory ---data===MemoryTask ---Isolate exit with last message to sendPort
I/flutter ( 4910): 12:58:32:92:cjf---testIsolateMemory ---i=1, intObject=1

我们从输出log可以看出,

  • MemoryTask Isolate中,每隔 1s 就给顶层变量加 1,但是,Root Isolate中的变量并不会同步改变;

  • Root Isolate中 5s 后给顶层变量加 1,也只影响了Root Isolate中读取的顶层变量值,不会影响到MemoryTask Isolate中读取的顶层变量值。

由此可见,跨Isolate数据共享只能通过port的方式。Isolate中的内存是独立的,它们不存在数据共享,当然也就不需要处理共享数据的线程同步问题。

Isolate中内存独立的特点,和Java中的线程局部存储ThreadLocal有点类似。AndroidLooper类里的静态变量sThreadLocal,就属于线程局部存储,不同线程中调用Looper.prepare设置到sThreadLocal中的Looper对象各不相同,取出来使用的对象也不相同。我们可以参考对比其源码,如下:

//Looper.java
public final class Looper {static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();......private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}......
}

通过Dart源码也可以看到,Isolate的创建过程,包括了创建Isolate结构体,在堆中分配线程内存,创建线程和使用线程池等代码,也进一步说明了Isolate是具有独立内存的线程。

了解完Isolate线程模型,我们就明白了:我们所写的Dart代码默认是运行在 Root Isolate中,而Root Isolate是运行在UI Runner上的,所以如果我们写的Dart代码过于耗时,必然导致负责管理绘制的UI Runner不能及时刷新页面,导致页面卡顿。因此,在Flutter应用中也需要使用多线程去处理耗时任务。

但是已有的使用经验中,我们并没有单独创建Isolate去处理网络请求任务,这与上述结论不就矛盾了吗?

我们先继续学习Flutter的异步原理,再来解答这个问题。

四、异步原理

Flutter中,如果不单独创建Isolate的话,可以说是单线程模型,它通过线程异步方案支持大量并发操作。

大家容易把异步,并行和并发这几个概念搞混。我们先来了解下并行和并发的概念:

  • 并行指的是多个CPU,在同一时间里执行多个任务;

  • 并发指的是一个CPU,轮换着去处理多个任务,由系统来管理任务的切换。

并行的实现,必须由多线程来完成。多线程的方式可以利用多CPU的并行优势,同时执行多个任务。一般通过线程池来管理和复用大量线程。

并发的实现多采用单线程+非阻塞+事件通知的方式来完成。因为线程切换的消耗是比较大的,不适合大量创建,所以并发的实现多采用单线程。阻塞式中断是在执行任务时将线程阻塞,等待执行完成后再恢复线程执行,无法达到异步效果。而非阻塞式中断是在执行任务时,保存当前上下文,不等待任务结果,继续调度当前线程的其它任务。调度当前线程的其它任务就依赖于事件通知。

异步是相对于同步来说,它指程序的执行顺序与任务的排列顺序是不一致的。异步属于并发,不属于并行。

上面的描述,可能比较抽象,不易理解,我们继续通过图和示例来学习Flutter的异步原理。

4.1 事件循环和消息队列

Flutter中,Isolate是通过事件循环和消息队列来实现异步的。每个Isolate 包含一个事件循环以及两个事件队列:

  • Event Loop:事件循环,负责无限循环读取微任务队列和事件队列进行处理;

  • Microtask queue:微任务事件队列,优先级比Event queue高,应用可以向 Isolate 添加微任务;

  • Event queue:普通事件队列,包括IO事件,绘制事件,手势事件,及应用添加的外部事件。

事件循环和事件队列的执行流程如下图:

7f1b086ee4577d3eb70e593be39597e9.png

Root Isolate中,Event queue包括了绘制事件和手势事件,如果它们不能得到及时处理,会导致渲染、手势响应延时,出现卡顿现象。

为了保证渲染和手势得到及时响应,我们应该尽量不要向Microtask queue中添加事件,因为它的处理优先级比Event queue要高。

即便向Event queue中添加事件时,也不能添加过于耗时的事件,避免影响后续的渲染和手势事件得不到及时响应,影响用户使用体验。

我们可以通过FutureawaitEvent queue中插入任务,也可以通过scheduleMicrotask Microtask queue添加任务。

下面通过举例,进一步进行说明:

// test_isolate.dart
void testMicroTask() async{print_cjf('testMicroTask start');new Future(() => print_cjf('future 1-1')) //创建异步任务 future 1.then((_){new Future(()=>print_cjf('future 2')); //创建异步任务 future 2scheduleMicrotask(() =>print_cjf('microtask 3'));//创建异步任务 microtask 3。比同层的 future 2 先执行print_cjf('future 1-2');}).then( (_)=>print_cjf('future 1-3') );scheduleMicrotask(() =>print_cjf('microtask 4'));//创建异步任务 microtask 4。 比同级别的 future 1 先执行print_cjf('testMicroTask 1');await funDelay(); //创建异步任务 funDelay。调用耗时方法,使用 await 等待其结果返回print_cjf('testMicroTask end'); // 异步执行
}Future<int> funDelay() async{ //声明异步方法, 必须使用 asyncprint_cjf('funDelay start'); // 同步执行await Future.delayed(Duration(seconds: 1)); //见 log1,调用 1s 耗时方法,使用 await 等待// await Future.delayed(Duration(milliseconds: 1)); //见 log2,调用 1ms 耗时方法,使用 await 等待print_cjf('funDelay end'); // 异步执行return 2;
}//输出 log1
I/flutter ( 4910): 17:51:25:216:cjf---testMicroTask start
I/flutter ( 4910): 17:51:25:216:cjf---testMicroTask 1 //同步执行
I/flutter ( 4910): 17:51:25:216:cjf---funDelay start //同步执行I/flutter ( 4910): 17:51:25:217:cjf---microtask 4 //第一层 microtask
I/flutter ( 4910): 17:51:25:217:cjf---future 1-1 //第一层 future
I/flutter ( 4910): 17:51:25:218:cjf---future 1-2 //第一层 future 的链式调用
I/flutter ( 4910): 17:51:25:218:cjf---future 1-3 //第一层 future 的链式调用I/flutter ( 4910): 17:51:25:218:cjf---microtask 3 //第二层 microtask
I/flutter ( 4910): 17:51:25:218:cjf---future 2 //第二层 futureI/flutter ( 4910): 17:51:26:218:cjf---funDelay end //属于第一层 future,但由于时间条件不满足,比第二层future更后执行了
I/flutter ( 4910): 17:51:26:219:cjf---testMicroTask end //属于第一层 future//输出 log2
I/flutter ( 9669): 17:58:34:948:cjf---testMicroTask start
I/flutter ( 4910): 17:58:34:950:cjf---testMicroTask 1 //同步执行
I/flutter ( 9669): 17:58:34:950:cjf---funDelay start //同步执行I/flutter ( 9669): 17:58:34:951:cjf---microtask 4//第一层 microtask
I/flutter ( 9669): 17:58:35:94:cjf---future 1-1//第一层 future
I/flutter ( 9669): 17:58:35:95:cjf---future 1-2//第一层 future 的链式调用
I/flutter ( 9669): 17:58:35:96:cjf---future 1-3//第一层 future 的链式调用I/flutter ( 9669): 17:58:35:96:cjf---microtask 3//第二层 microtaskI/flutter ( 9669): 17:58:35:97:cjf---funDelay end //属于第一层 future
I/flutter ( 9669): 17:58:35:97:cjf---testMicroTask end //属于第一层 futureI/flutter ( 9669): 17:58:35:97:cjf---future 2//第二层 future

上述例子中,在funDelay等待 1s 和 1ms 的情况,分别输出了log1log2。通过对比输出的log, 我们可以清晰的看到:

  • MicrotaskFuture添加的任务都会被异步执行;

  • 同层的Microtask会比Future优先被处理。因为new Future创建的异步事件被添加到了Event queue,而scheduleMicrotask创建的异步事件被添加到了MicroTask queue中,而事件循环总是会优先处理 MicroTask queue中的事件;

  • await funDelay后的代码"funDelay end",属于第一层future

  • await的运行受返回结果的时机影响:‍

  • log1中,await funDelay需要 1s 时,事件循环处理到它时,发现时间条件不满足后跳过,所以"funDelay end"会比第二层的"future 2"后执行;

  • log2中,如果await funDelay只需要 1ms 时,"funDelay end"就会比第二层的"future 2"先执行;

  • 但无论是log1还是log2"funDelay end"都会比第二层的"microtask3" 后执行,因为microtask总是会被优先执行,即使更后被添加到microtask队列。

在上述例子中,await funDelay调用也会将后续代码处理事件,如 "testMicroTask end",添加到Event queue,属于第一层事件,会比第二层事件"future 2"Event queue中的位置更靠前,如果条件符合的话,会比第二层的 "future 2" 事件更先被执行。

"microtask 3"虽然是第二层 MicroTask,但是在第一层"future 1"执行时被添加到Microtask queue中。当"future 1 "执行完,回到Event loop循环时,会优先处理Microtask queue中的事件,所以第二层的"microtask 3"会比 第一层的"funDelay end"先执行。

下面用图来更形象地描述上述例子,我们可以更清晰的看到事件队列的添加和消费情况:

203752134385fbfcedcfe76f2c51648f.png

通过上面的示例,我们可以看到在Flutter中进行异步调用,一般需要用到三个关键词:Futureasyncawait。其中asyncawait需要一起使用。

  • FutureFuture表示异步操作的返回结果,可以通过then处理返回结果。和Java中的Future功能差别巨大,注意区别使用;

  • async:标记某个方法为异步方法,在声明方法的时候使用。其返回值是Future类型;

  • await:表示等待某个异步方法的结果,一般调用耗时异步方法时使用。

下面我们继续深入Futureasyncawait的使用和原理学习。

4.2 Future

Dart中可以通过Future进行异步操作,Future异步的特点是提供了链式调用,可以解决回调地狱,异常捕获,和代码执行依赖等问题。

Future<T>表示一个指定类型的异步操作结果,如果不需要结果可以使用Future<void>。我们可以直接使用Future创建一个异步操作结果,也可以通过调用async方法得到一个异步操作结果。

  • 当创建一个异步事件时,会直接返回一个Future,后续代码可以继续执行,不会被阻塞;

  • Future中的结果返回时,如果注册了then结果回调,onValue回调会拿到成功的返回值;

  • Future中出现执行异常时,如果注册了catchError失败回调,onError回调会拿到失败的异常信息和错误栈 。

我们通过一个简单的例子,进一步了解Future

// test_isolate.dart
void testFuture() async{print_cjf('testFuture start');Future f1 = new Future(() {print_cjf('future 1-1');return "aaa";}).then((value) {print_cjf('future 1-2' + "---$value");// throw 'bbb Error!'; //见 log2-1,log2-2return "bbb"; //见 log1}).then((value) => print_cjf('future 1-3' + "---$value") ).catchError((err, stackTrace) {print_cjf('Caught $err' +"---$stackTrace");return "ccc";}, test: (err) {print_cjf('Test $err');return true; //见 log2-1。test return true:onError 会执行,后续 then 的 callback 也会被调用,不会抛出 Unhandled Exception// return false; //见 log2-2。test reture false:onError 不会执行,后续 then 的 callback 不会被调用,且抛出[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: bbb Error!。});print_cjf('testFuture 1');Future f2 = new Future(() => print_cjf('future 2-1'));Future f3 = new Future(() => print_cjf('future 3-1'));print_cjf('testFuture 2');f3.then((value) => print_cjf('future 3-2' + "---$value"));f2.then((value) => print_cjf('future 2-2' + "---$value"));f1.then((value) => print_cjf('future 1-4' + "---$value"));print_cjf('testFuture end');
}//输出 log1
I/flutter (22807): 19:5:32:221:cjf---testFuture start //同步执行
I/flutter (22807): 19:5:32:222:cjf---testFuture 1  //同步执行
I/flutter (22807): 19:5:32:223:cjf---testFuture 2 //同步执行
I/flutter (22807): 19:5:32:223:cjf---testFuture end //同步执行I/flutter (22807): 19:5:32:225:cjf---future 1-1  //异步执行
I/flutter (22807): 19:5:32:226:cjf---future 1-2---aaa //第一步返回的结果
I/flutter (22807): 19:5:32:228:cjf---future 1-3---bbb //第二步返回的结果
I/flutter (22807): 19:5:32:229:cjf---future 1-4---null //then 后调用,却先执行。没有异常,第三步没有返回,默认为 nullI/flutter (22807): 19:5:32:231:cjf---future 2-1
I/flutter (22807): 19:5:32:233:cjf---future 2-2---null //then 后调用,却先执行
I/flutter (22807): 19:5:32:235:cjf---future 3-1
I/flutter (22807): 19:5:32:236:cjf---future 3-2---null //then 先调用,后先执行//输出 log2-1
I/flutter (22807): 19:6:58:613:cjf---testFuture start
I/flutter (22807): 19:6:58:615:cjf---testFuture 1
I/flutter (22807): 19:6:58:615:cjf---testFuture 2
I/flutter (22807): 19:6:58:615:cjf---testFuture endI/flutter (22807): 19:6:58:617:cjf---future 1-1
I/flutter (22807): 19:6:58:618:cjf---future 1-2---aaa
I/flutter (22807): 19:6:58:619:cjf---Test bbb Error! //test 返回 ture,onError 会执行
I/flutter (22807): 19:6:58:621:cjf---Caught bbb Error!---#0 testFuture.<anonymous closure> (package:supermarie/testPages/test_isolate.dart:269:9)
I/flutter (22807): <asynchronous suspension>
I/flutter (22807): #1 testFuture.<anonymous closure> (package:supermarie/testPages/test_isolate.dart:276:20)
I/flutter (22807): <asynchronous suspension>
I/flutter (22807): 19:6:58:621:cjf---future 1-4---ccc //onError 捕获异常后,返回结果作为下一步 then 的入参I/flutter (22807): 19:6:58:622:cjf---future 2-1
I/flutter (22807): 19:6:58:623:cjf---future 2-2---null
I/flutter (22807): 19:6:58:624:cjf---future 3-1
I/flutter (22807): 19:6:58:625:cjf---future 3-2---null//输出 log2-2
I/flutter (22807): 19:7:48:994:cjf---testFuture start
I/flutter (22807): 19:7:48:996:cjf---testFuture 1
I/flutter (22807): 19:7:48:997:cjf---testFuture 2
I/flutter (22807): 19:7:48:998:cjf---testFuture endI/flutter (22807): 19:7:49:0:cjf---future 1-1
I/flutter (22807): 19:7:49:2:cjf---future 1-2---aaa
I/flutter (22807): 19:7:49:4:cjf---Test bbb Error! //test 返回 false,onError 不会执行,后续的 then 回调也不会被执行,"future 1-4"不会被打印出来
E/flutter (22807): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: bbb Error!
E/flutter (22807): #0 testFuture.<anonymous closure> (package:supermarie/testPages/test_isolate.dart:269:9)
E/flutter (22807): <asynchronous suspension>
E/flutter (22807): #1 testFuture.<anonymous closure> (package:supermarie/testPages/test_isolate.dart:276:20)
E/flutter (22807): <asynchronous suspension>
E/flutter (22807): 
I/flutter (22807): 19:7:49:7:cjf---future 2-1
I/flutter (22807): 19:7:49:8:cjf---future 2-2---null
I/flutter (22807): 19:7:49:10:cjf---future 3-1
I/flutter (22807): 19:7:49:12:cjf---future 3-2---null

从例子,我们可以看到:

  • 所有Future创建的任务都是异步执行的,即创建时机和运行时机不同。上例的所有log中,"future 1-1"都在"testFuture end"之后执行;

  • future创建异步任务的次序,就是其加入到Event queue事件队列的次序,也是被异步执行的次序。输出log可以看到"future1-1","future2-1","future3-1"都是按照创建次序执行;

  • future通过then来获取异步结果,then支持链式调用。then注册的onValue结果回调,会在异步结果返回后立即执行,且后面的then的入参即是前面then回调的返回值。输出log1"future1-2---aaa"打印了"future1-1"中的返回值,而"future1-3---bbb"打印了"future1-2"中的返回值;

  • future注册then结果回调的时机,不影响其被回调的时机。见log1,"future 1-4","future 2-2","future 3-2"的执行次序,和其调用次序没有关系,只和其依赖的future的创建次序有关;

  • Future通过catchError注册的onError回调,受test回调的返回值影响,我们对比log2-1log2-2看:‍

  • 如果test回调返回true,异常会被onError处理,并返回值作为下一步then 的入参;

  • 如果test回调返回false,异常不会被onError处理,还会导致后续Then 回调不被执行,如"future 1-4"不会被执行到。

Future的链式调用,很好的解决了调用依赖导致的回调嵌套问题,我们以Kotlin中的登入流程为例做对比,源码如下:

// TestCallbackActivity.kt
class TestCallbackActivity extends BaseActivity  {// 登入fun login(name:String, pwd:String, onSuccess:(uid:String) -> Unit, onError:() -> Unit):Unit {}// 获取用户信息, 用户首选 tabfun getUserInfo(uid:String, onSuccess:(UserBean) -> Unit, onError:() -> Unit):Unit {}// 显示用户信息fun showUserInfo(user:UserBean):Unit {}// 获取用户首选 tab 动态流fun getFeedListByTab(tab:Int, onSuccess:(feedList:List<NewFeedBean>) -> Unit, onError:() -> Unit):Unit { }// 显示用户首选 tab 动态流fun showFeedList(feedList:List<NewFeedBean>):Unit {}// 获取用户第一个动态音乐fun getBackgroudMusic(mid:String, onSuccess:(playUrl:String) -> Unit):Unit {}// 播放用户动态音乐fun playMusic(playUrl:String):Unit {}override fun onResume() {login("name", "pwd", onSuccess = {//第一层回调,登入成功getUserInfo(it, onSuccess =  {//第二层回调,获取用户信息成功showUserInfo(it)getFeedListByTab(it.mainTab, onSuccess = {//第三层回调,获取用户主页 feed 流成功showFeedList(it)getBackgroudMusic(it[0].mid){//第四层回调,获取 tab 流默认背景音乐成功playMusic(it)}}, onError = {})}, onError = {})}, onError = {})}
}

因为登入,获取用户信息,获取默认主页feed流,和主获取页流背景音乐这几个流程是有强依赖关系的,后面的步骤依赖前一个步骤返回的参数,导致每多一个步骤,就会多一层回调嵌套,非常影响可读性。

同样的逻辑,我们再通过FlutterFuture实现,见如下源码。

// testFutureCallback.dart// 登入
Future<String> login(String name, String pwd) {}
// 获取用户信息, 用户首选 tab
Future<UserBean> getUserInfo(String uid) {}
// 显示用户信息
void showUserInfo(UserBean user) {}
// 获取用户首选 tab 动态流
Future<List<NewFeedBean>> getFeedListByTab(Int tab) {}
// 显示用户首选 tab 动态流
void showFeedList(List<NewFeedBean> feedList) {}
// 获取用户第一个动态音乐
Future<String> getBackgroudMusic(String mid) {}
// 播放用户动态音乐
void playMusic(String playUrl) {}void testFuture2() async{new Future(() {return login("name", "pwd");}).then((uid) {return getUserInfo(uid);}).then((user) {showUserInfo(user);return getFeedListByTab(user.mainTab);}).then((feedList) {showFeedList(feedList)return getBackgroudMusic(feedList[0].mid)}).then((playUrl) {playMusic(playUrl)}).catchError((err, stackTrace) {// 根据 err 信息,处理异常});
}

可以看到,Future通过then明确代码块执行的依赖关系,不但消除了多层回调嵌套,也简化了方法的定义和错误处理。当然Kotlin中也可以通过协程等方式解决回调嵌套问题。

 4.3 async和await

Dart中还可以通过asyncawait实现异步操作。async 表示开启一个异步操作,可以返回一个Future结果。如果没有返回值,则默认返回一个Future<void>

asyncawait本质上就是Dart对异步操作的一个语法糖,可以减少异步调用的嵌套调用。这个语法是在ES7标准中推出的,Dart中的设计和JS相同,是同步风格的异步实现。

我们通过数据加载的示例,来进一步了解async,await操作:

class _DemoPageState extends State<DemoPage> {List widgets = [];@overridevoid initState() {super.initState();loadData();print("initState end");}Future<String> loadData() async {print("loadData start");Response response = await getNetData(); //位置 1setState(() {widgets = json.decode(response.body); //位置 2});return response.body;}Future<Response> getNetData() async {String requestURL = 'https://hy.sohu.com/testGetStringList1';Client client = Client(); //位置 3Future<Response> response = client.get(requestURL); //io 异步,后续章节会详细分析return response; //位置 4}
}

在代码示例中,执行到loadData方法时,会同步进入方法内部进行执行,当执行到位置 1 的await时,就会停止loadData方法内后续的代码的执行,返回到外部调用者initState继续执行后面的print语句。当位置 1 的await有返回后,会从位置 1 处继续执行response的赋值操作及后续的setState位置 2 的语句。

那么,await是怎么做到阻塞当前方法,却不阻塞调用者继续运行的呢?以及,getNetData方法中,位置 3 处的代码是被同步执行的,还是被异步执行的呢?

先来分析下第一个问题,我们知道在程序执行过程中,离开当前的调用位置有两种方式:

  • 执行return返回,当前函数在调用栈中的局部变量、形参等状态会被销毁;

  • 继续调用其他函数,需要保先存当前函数的变量和执行位置,其它函数调用返回后再恢复变量继续执行。保存变量一般有 2 种方式:

  • 一种,会将变量继续保存在栈区,在回到指针指向的离开位置时,会继续从栈中取出变量使用;

  • 另一种,则在执行当前函数时就将变量直接分配到堆区,当再次回到当前位置时,还会继续从堆区中获取变量。async,await就属于这种方式。

Flutter中,async函数也有自己的上下文环境。当执行到await时,会保存当前的上下文,并将当前位置标记为待处理任务,并将待处理任务放入当前Isolateevent队列中。在每个事件循环时询问这个任务是否满足执行条件,如果需要进行处理,就恢复上下文,从上次离开的位置继续执行。

所以说,await并没有开启新的Isolate,只是把await后的代码封装成待处理任务,放到当前Isolate的消息队列中,然后继续运行当前任务。因此await做到了不阻塞调用者的执行。

第二个问题,我们通过如下示例进一步说明:

// test_isolate.dart
void testAwait(){ // 测试 await 中断效果print_cjf('testAwait start');fun1();fun2();//async 方法,会 await 一个异步结果fun3();fun4();//async 方法,但是并没有 await 异步结果print_cjf('testAwait end');
}void fun1(){print_cjf('fun1');
}void fun2() async{ //声明异步方法 使用 asyncprint_cjf('fun2 start');await Future.delayed(Duration(seconds: 2)); //使用 await,会异步等待结果返回print_cjf('fun2 end');
}void fun3(){print_cjf('fun3');
}void fun4() async{ //声明异步方法 使用 asyncprint_cjf('fun4 start');Future.delayed(Duration(seconds: 5)); //没有使用 await, 不会等待结果返回print_cjf('fun4 end');
}// 输出 log 
I/flutter (24288): 16:34:25:354:cjf---testAwait start
I/flutter (24288): 16:34:25:354:cjf---fun1
I/flutter (24288): 16:34:25:354:cjf---fun2 start // await 前的代码被同步执行了I/flutter (24288): 16:34:25:355:cjf---fun3 // 不需要等待 fun2 方法执行完,就被同步执行了
I/flutter (24288): 16:34:25:355:cjf---fun4 start // 被同步执行了
I/flutter (24288): 16:34:25:355:cjf---fun4 end// 同步执行。并没有在 fun4 start 后等 5s 再输出。 但是在 5s 内程序不会退出I/flutter (24288): 16:34:25:355:cjf---testAwait end //fun1,fun2,fun3,fun4 并没有阻塞 testAwait 后续代码的执行I/flutter (24288): 16:34:27:359:cjf---fun2 end//fun2 start 后, 等 2 秒输出 end

这个示例中,我们通过交替调用async和非async函数,来理解同步调用。我们对比观察fun2fun4log的输出情况,可以发现:

  • fun2 中,await前的代码"fun2 start""fun3"前执行,所以这行代码是被同步执行的;

  • fun2 中,await Future.delayed,阻塞了 "fun2 end"的同步执行,它会等待 delay 2 秒后才被执行,所以最后被执行;

  • fun4中,Future.delayed没有使用await,函数执行没有被阻塞,"fun4 end""testAwait end"前执行,所以是被同步执行的,即Future.delayed创建的异步任务不会打断当前函数执行。

这个示例中,Future.delayed函数本身是同步执行的,它创建一个异步任务,放入当前 Isolate event队列后返回。而await Future.delayed则需要等待创建的异步任务执行完成后,再恢复当前函数后续代码的执行。

通过log分析可知:await关键字,把同一个函数分割为同步执行部分和异步执行两部分await关键字之前的代码和调用者在同一个任务中同步执行。而没有await调用的async函数,代码都是同步执行的。

所以在_DemoPageState例子中,getNetData 函数位置 3 的代码会和调用者在同一个任务中被同步执行。而loadData函数位置 2 的代码属于待处理任务,会被异步执行。

在 4.1 章节的testMicroTask微任务例子中,我们也可以看到funDelay异步方法await前的"funDelay start"代码是被同步执行的。

对比testFuture例子中Future创建和then回调的例子,从log中也可以知道,Future中创建的异步任务,都是被异步执行的。

await的使用效果和Android Kotlin协程很类似,launchasync 只是启动协程job,job await要等待协程结果,才会真正阻塞当前函数后续代码的执行,但是它不阻塞调用线程中其它代码的运行。虽然Kotlin协程的异步原理和Flutter不同,但通过单线程异步支持大量并发操作的设计思想类同,可以一起对比学习。

Futureasync,await两种异步方式,在我们的项目中都有频繁使用,各有优点和适用场景,我们可以通过例子可以更直观的说明:

main( ){new Future.then(funA()).then(funB()); // then 明确表现出了 funB 依赖 funA
}main( ) async {await funA( );await funB(); // funB 依赖 funA 的关系不明显
}

Future能明确方法执行的依赖次序,其它开发者不容易破坏这层依赖逻辑。而 await没有明确体现方法之间的依赖关系。对于没有通过返回值直接体现依赖的情况下,其他开发者不容易理解到这层依赖,会导致后续代码难以维护。

await的风格,代码更简洁美观,如果没有依赖关系,更推荐这种风格。

 4.4 IO异步

在前面章节中,testIsolatetestFuturetestAwait例子中,这些函数和新建异步任务都是在Root Isolate中运行的。以及_DemoPageState例子中加载网络数据的getNetData耗时方法也是在Root Isolate中运行的。

但是Task Runner章节中明确UI Runner负责界面的更新,也运行在Root Isolate中。为了避免界面卡顿,必然不能在Root Isolate的异步任务中执行耗时任务。

那么像网络请求,文件读取,海量计算,图片编解码等耗时函数的调用,是不是都需要创建新的Isolate来处理呢?

我们在实际项目中,会大量使用网络相关耗时IO操作。下面通过网络请求过程为例,进一步分析IO异步:

//home_page.dart
class KKHomePageState extends BaseThemeState<KKHomePage> with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {..........@overridevoid initState() {super.initState();.......if (_login) {print_cjf("h02---" + Isolate.current.debugName.toString());_refresh(); //准备刷新数据print_cjf("h022---" + Isolate.current.debugName.toString());}}......Future<void> _refresh() async {print_cjf("h5---" + Isolate.current.debugName.toString());await _viewModel.getList(15, 1); // 获取网络数据,等待数据结果。虽然没有直接使用返回值,但是此方法后续的代码中依赖网络数据,所以此处必须 await。但如果后续代码是通过监听的方式来使用网络数据,此处可去掉 await。print_cjf("h6---" + Isolate.current.debugName.toString());homeAllList.currentState?.resetDataAndRefresh();........}
}// subscribe_list_vm.dart
Future<void> getList(int? limit, int? status) async {........print_cjf("h7---" + Isolate.current.debugName.toString());await NetManager.getSubjectSubscribeClient().getMySubscribed(BaseRequestParams().makeSignMap(urlMap: requestMap)) //获取用户订阅的主题.then((value) {print_cjf("h8---" + Isolate.current.debugName.toString());......}........
}// log 输出:
I/flutter (13129): 15:32:23:235:cjf---h02---main // main_page 中的 initstate,调用_refresh 之前。同步执行。
I/flutter (13129): 15:32:23:236:cjf---h5---main // 进入 _refresh() async {}方法,调用_viewModel.getList 之前。同步执行。
I/flutter (13129): 15:32:23:238:cjf---h7---main // 同步执行。await _viewModel.getList(15, 1); 进入到 subscribe_list_vm 中的 getList 方法。同步执行。
I/flutter (13129): 15:32:23:239:cjf---h022---main // 同步执行。main_page 中的 initstate,调用_refresh 之后。同步执行。I/flutter (13129): 15:32:23:742:cjf---h8---main // await NetManager.getSubjectSubscribeClient().getMySubscribed 方法返回。异步步执行。
I/flutter (13129): 15:32:23:760:cjf---h6---main // 进入 _refresh() async {}方法,调用_viewModel.getList 之后。异步执行。

log可以看出网络请求getList async方法的代码也是在 Root Isolate 执行的,并没有启动新的Isolate

await等待aysnc方法结果时,会产生Isolate内的异步调度,即把待处理任务h6h8放到当前 Isolate的消息队列中,然后继续运行当前任务打印"h022---main"

那么NetManager.getSubjectSubscribeClient通过retrofit获取网络数据的耗时 IO也是在Root Isolate中执行的吗?

我们通过网络限速,使得获取网络数据的接口 5s 后返回结果。验证发现KKHomePage在这 5s 中可以流畅地进行操作,且能打印出Root Isolate中其它异步任务执行的log,这说明网络耗时IO没有在Root Isolate中执行,这是什么原因呢?

这是因为网络请求过程中,当运行到系统IO接口时,会将IO任务交给系统处理(属于系统的线程),并将异步任务加入到事件队列,然后Root Isolate回到事件循环,获取事件队列中的下一个任务进行处理。

Flutter文档中,没有明确哪些接口属于系统IO接口,但是通过Dart SDK的源码跟踪,我们可以发现,网络请求HttpClient最后的IO操作是在dart虚拟机中的native socket中实现的,而socket中使用了线程池来管理线程,并把IO Task分配到各子线程中运行。

所以,对于有系统IO操作的耗时任务,如网络请求,文件读取等,可以使用await,等待耗时IO操作的异步结果。

而执行大量高CPU的运算类耗时任务,如耗时计算,编解码等,即便使用await,但所有代码都运行在Root Isolate中,所以仍然会导致Root Isolate没法及时处理其他异步任务,从而导致UI卡顿。所以官方推荐高CPU耗时任务,应该使用新的Isolate去执行,如前面章节的testCompute例子所示。

通过上面的分析,我们就可以很好地理解,为什么官方只说Isolate不支持高CPU操作,而没有说不支持耗时IO操作了。

开辟新的Isolate的成本比较高,所以对于不太耗时的任务,还是建议用Futureawait的方式。那么,什么样的任务算高CPU的耗时任务呢?这个没有绝对标准,建议毫秒级的任务用Futureawait,而百毫秒级的任务,比如图片处理,数据加密等,开启新的Isolate

 4.5 Js异步

Flutter中的线程异步和浏览器中异步原理是一样的,而且asyncawait语法糖也是在ES7标准中推出的,通过对比,我们可以进一步对Flutter的异步加深了解。

JavaScript的世界中,所有代码都是单线程执行的,即通常情况下,Js代码不必考虑多线程。JS中,异步的目的是为了提高CPU的执行效率,提高用户体验。它和同步一样,运行在同一线程中。它和同步的差别在于,同一流水线上各个代码片段的执行顺序不同。

下面我们通过示例来看一下JS中的异步:

// test.js
async function fun1() {var vConsole = new VConsole();console.log('cjf--- fun1---1')var a = 123console.log('cjf--- fun1---2')var b = await fun2() //等待 fun2 的异步结果console.log('cjf--- fun1---3')var c = bconsole.log('cjf--- fun1---4')return 1;
}async function fun2() {console.log('cjf--- fun2---1')var a = 123console.log('cjf--- fun2---2')var b = await fun3() //等待 fun3 的异步结果console.log('cjf--- fun2---3')var c = bconsole.log('cjf--- fun2---4')
}async function fun3() {console.log('cjf--- fun3---1')var a = 123console.log('cjf--- fun3---2')var b = await makeRequest()  //等待网络请求的异步结果console.log('cjf--- fun3---3')var c = bconsole.log('cjf--- fun4---4')return 3;
}function makeRequest() {console.log('cjf--- makeRequest---1')httpRequest = new XMLHttpRequest();if (!httpRequest) {alert('Giving up :( Cannot create an XMLHTTP instance');return false;}httpRequest.onreadystatechange = alertContents; // 注册异步回调httpRequest.open('GET', 'test.html'); // 耗时 IO 接口,会切换到引擎新线程console.log('cjf--- makeRequest---2')httpRequest.send(); // 耗时 IO 接口,会切换到引擎新线程console.log('cjf--- makeRequest---3')return 4
}function alertContents() { // 数据 ready 回调console.log('cjf--- alertContents---1---' + httpRequest.readyState)if (httpRequest.readyState === XMLHttpRequest.DONE) {console.log('cjf--- alertContents---2')if (httpRequest.status === 200) {alert(httpRequest.responseText);console.log('cjf--- alertContents---3---' + httpRequest.responseText)} else {alert('There was a problem with the request.');console.log('cjf--- alertContents---4---a problem with the request.')}}
}fun1() //调用方法。// log 输出
cjf--- fun1---1
cjf--- fun1---2
cjf--- fun2---1
cjf--- fun2---2
cjf--- fun3---1
cjf--- fun3---2 //同步执行cjf--- makeRequest---1 //同步执行
cjf--- alertContents---1---1 //httpRequest.open 会回调 alertContents
cjf--- makeRequest---2 //异步执行
cjf--- makeRequest---3 //异步执行cjf--- fun3---3 //异步执行
cjf--- fun3---4 //异步执行
cjf--- fun2---3 //异步执行
cjf--- fun2---4 //异步执行
cjf--- fun1---3 //异步执行
cjf--- fun1---4 //异步执行cjf--- alertContents---1---4 //httpRequest.send 会异步回调 alertContents
cjf--- alertContents---2 //httpRequest.send 会异步回调 alertContents
cjf--- alertContents---4---a problem with the request. //httpRequest.send 会异步回调 alertContents

从上述例子可以看出,Func1Func2Func3中的a赋值操作都是同步执行的,bc的赋值操作是异步执行的。

在浏览器运行await时, 会一层一层运行到游览器引擎新建线程接口(比如Ajax请求接口,XMLHttpRequest.send),调用系统IO接口前的代码片段是被同步执行的,之后的代码片段则需要等到异步结果返回后,才能继续执行。

所以await实际上并不是异步执行的 "分界线",即 await前面的代码同步执行,await后面的代码异步执行。因为await后面的异步函数代码中的部分内容,也可能被同步执行,代码会一直运行到系统IO接口处,才算到了分界线。但是从执行的结果来看,await确实达到了 “阻塞等待” 的效果。

在使用时,我们并不关心异步函数中的部分代码被同步执行了还是被异步执行了,我们关心的是await的异步数据必须返回,才能再继续执行下面的代码。但对原理的深入理解,可以提高我们对系统设计思想的运用能力。

小结

通过上面的学习,我们对Flutter中的线程模型和单线程异步原理有了更深入的理解,开发时也能更好更安全地运用FlutterIsolateFutureawait

我们最后总结下文章的核心内容:

  • IsolateDart平台对线程的实现方案。和普通Thread不同,Isolate 拥有独立的内存,由线程和独立内存构成。Isolate线程之间并不存在资源抢夺的问题,所以也不需要线程同步机制;

  • 简单耗时任务,可以使用简版Isolatecompute

  • Isolate通过事件循环和消息队列来实现单线程异步并发,这比多线程异步并发要轻便;

  • 可以通过Future进行单线程异步并。Future异步的特点是提供了链式调用,可以解决回调地狱,异常捕获和代码执行依赖等问题;

  • 也可以通过asyncawait实现单线程异步并发,它是同步风格的异步操作,比Future更简洁美观;

  • Root Isolate支持IO耗时操作,但不支持高CPU耗时操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/15074.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

stable diffusion打造自己专属的LORA模型

通过Lora小模型可以控制很多特定场景的内容生成。 但是那些模型是别人训练好的&#xff0c;你肯定很好奇&#xff0c;我也想训练一个自己的专属模型&#xff08;也叫炼丹&#xff5e;_&#xff5e;&#xff09;。 甚至可以训练一个专属家庭版的模型&#xff08;family model&…

RT1052 的周期定时器

文章目录 1 PIT 周期中断定时器2 PIT定时器的使用3 PIT定时器配置3.1 PIT 时钟使能。3.1.1 CLOCK_EnableClock 3.2 初始化 PIT 定时器3.2.1 PIT_Init 3.3 设置 通道 0 的 加载值3.3.1 PIT_SetTimerPeriod 3.4 使能 通道 0 的中断3.4.1 PIT_EnableInterrupts 3.5 开启 PIT 定时器…

PysparkNote006---pycharm加载spark环境

pycharm配置pyspark环境&#xff0c;本地执行pyspark代码 spark安装、添加环境变量不提了 File-Settings-Project-Project Structure-add content root添加如下两个路径 D:\code\spark\python\lib\py4j-0.10.7-src.zipD:\code\spark\python\lib\pyspark.zip 2023-07-26 阴 于…

Redis缓存预热

说明&#xff1a;项目中使用到Redis&#xff0c;正常情况&#xff0c;我们会在用户首次查询数据的同时把该数据按照一定命名规则&#xff0c;存储到Redis中&#xff0c;称为冷启动&#xff08;如下图&#xff09;&#xff0c;这种方式在一些情况下可能会给数据库带来较大的压力…

AcWing 算法基础课二 数据结构 链表 栈 队列 并查集 哈希表

单链表. AcWing. 826.单链表 import java.util.Scanner; public class Main{static int[] e new int[100010];//结点i的值static int[] ne new int[100010];//结点i的next指针static int idx,head;//head是头结点&#xff0c;idx存当前已经用到了哪个点public static void i…

【简化程序设计】C++STL“容器适配器“之栈和队列

【STL】容器适配器之栈和队列 stack的介绍和使用stack的介绍stack的使用stack的模拟实现 queue的介绍和使用queue的介绍queue的使用queue的模拟实现 priority_queue的介绍和使用priority_queue的介绍priority_queue的使用priority_queue的模拟实现 容器适配器什么是容器适配器&…

基于x-scan扫描线的3D模型渲染算法

基于x-scan算法实现的z-buffer染色。c#语言&#xff0c;.net core framework 3.1运行。 模型是读取3D Max的obj模型。 x-scan算法实现&#xff1a; public List<Vertex3> xscan() {List<Vertex3> results new List<Vertex3>();SurfaceFormula formula g…

从使用回溯分割字符串的技巧到前向搜索

题目 131. 分割回文串 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 答案&#xff1a; class Solution {boolean[][] f;List<List<String>>…

【多线程中的线程安全问题】线程互斥

1 &#x1f351;线程间的互斥相关背景概念&#x1f351; 先来看看一些基本概念&#xff1a; 1️⃣临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源。2️⃣临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码&#xff0c;就叫做临界区。3️⃣互斥&…

【密码学】三、AES

AES 1、AES产生2、数学基础2.1有限域GF(2^8^)2.1.1加法运算2.1.2乘法运算2.1.3x乘运算2.1.4系数在GF(2^8^)上的多项式 3、AES算法描述3.1字节代换3.2行移位3.3列混合3.4轮密钥加3.5密钥扩展 1、AES产生 征集AES算法的活动&#xff0c;目的是确定一个非保密的、公开的、全球免费…

HCIP——重发布及路由策略实验

重发布及路由策略实验 一、实验拓扑二、实验要求三、实验思路三、实验步骤1、配置接口IP地址以及环回地址2、配置动态路由协议3、重发布4、更改接口类型5、配置路由策略 一、实验拓扑 二、实验要求 1、使用双点双向重发布2、所有路由器进行最佳选路3、存在备份路径&#xff0c…

软考05根据内存区域大小计算芯片数量

文章目录 前言一、原题二、解题思路1.计算内存区域的大小2.计算每个存储器芯片的容量3.计算芯片数量 总结 前言 从网上看题答案是有了&#xff0c;但是不知道具体的计算过程就很难受&#xff0c;不然下次还是不会&#xff0c;只能自己梳理了 一、原题 二、解题思路 1.计算内存…

Android开发之Fragment动态添加与管理

文章目录 主界面布局资源两个工具Fragment主程序 主界面布局资源 在activity_main.xml中&#xff0c;声明两个按钮备用&#xff0c;再加入一个帧布局&#xff0c;待会儿用来展示Fragment。 <?xml version"1.0" encoding"utf-8"?> <LinearLayo…

手机的python怎么运行文件,python在手机上怎么运行

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;手机上的python怎么运行程序&#xff0c;手机的python怎么运行文件&#xff0c;今天让我们一起来看看吧&#xff01; 1、python程序怎么在手机上运行 python语言应用很广泛&#xff0c;自己也很喜欢使用它&#xff0c;其…

iOS - 检测项目中无用类和无用图片

一、无引用图片检测 LSUnusedResources 安装插件 LSUnusedResources &#xff0c;用【My Mac】模拟器运行,如下图&#xff1a; Project Path 就是项目所在的路径&#xff0c;然后点击右下角 Search按钮&#xff0c;就可以看到被搜索出来的图片资源。 注意&#xff1a;这里被搜…

Linux——进程控制

目录 1. 进程创建 1.1 fork函数 1.2 fork系统调用内部宏观流程 1.3 fork后子进程执行位置分析 1.4 fork后共享代码分析 1.5 fork返回值 1.6 写时拷贝 1.7 fork常规用法 1.8 fork调用失败的原因 2.进程终止 2.1 进程退出场景 2.2 strerror函数—返回描述错误号的字符…

解决问题:python PermissionError: [WinError 5]拒绝访问

重要&#xff1a;关闭PyCharm Community Edition 2022.3等与python相关的编程程序找到按照python解释器的位置python->右键>属性>安全->点击组或用户名"中的Users->编辑点击"组或用户名"中的Users->把"完全控制"打钩->应用->…

Servlet文件的下载

第一种方法直接在前端使用超链接&#xff0c;也就是a标签 浏览器不能识别会直接下载&#xff08;像压缩文件不能直接下载&#xff09;&#xff0c;浏览器能识别&#xff0c;想要下载加一个download属性。download可以不写任何信息。 首先在web下建一个文件&#xff0c;放需要…

在Windows 10和11中恢复已删除的照片

可以在Windows 10或11上恢复已删除的照片吗&#xff1f; 随着技术的发展&#xff0c;越来越多的用户习惯在电子设备上存储照片。如果这些照片被删除&#xff0c;可能会给用户带来重大损失。当照片丢失时&#xff0c;您可能会想是否可以恢复已删除的照片&#xff1f; …

Kafka原理剖析

一、简介 Kafka是一个分布式的、分区的、多副本的消息发布-订阅系统&#xff0c;它提供了类似于JMS的特性&#xff0c;但在设计上完全不同&#xff0c;它具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性&#xff0c;适用于离线和在线的消息消费&#xff0c;如常规的…