Flutter 的线程模型和异步原理

news2024/11/15 20:42:10

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的主界面
  ......

  @override
  void initState() {
    super.initState();
    ......
 
    //测试 Future 中代码的运行所属 Isolate
    Future.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());
  }
    
  @override
  Widget 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 的发送端口传给 newIsolate
  var 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, 并发送结束消息给 sendPort2
      Isolate.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 的 SendPort1

I/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---main

I/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---aaaaaa

I/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 sendPort2

I/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 传过来的String
    print_cjf('testIsolateMemory ---'+ "data===$msg");
    //打印 Root Isolate 中的变量值
    print_cjf('testIsolateMemory ---'+ "i=$intValue, intObject=${intObject.get()}");
  });

  //5s后,给顶层变量加1
  Future.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,给顶层变量和局部变量都加1
  Timer.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, 并发送结束消息给 sendPort
      Isolate.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 end

I/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 中给顶层变量加1

I/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=1

I/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 2
          scheduleMicrotask(() =>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{ //声明异步方法, 必须使用 async
  print_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 //第二层 future

I/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//第二层 microtask

I/flutter ( 9669): 17:58:35:97:cjf---funDelay end //属于第一层 future
I/flutter ( 9669): 17:58:35:97:cjf---testMicroTask end //属于第一层 future

I/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-2
        return "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 后调用,却先执行。没有异常,第三步没有返回,默认为 null

I/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 end

I/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 end

I/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 {}
  // 获取用户信息, 用户首选 tab
  fun 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 = [];
  
  @override
  void initState() {
    super.initState();
    loadData();
    print("initState end");
  }
  
  Future<String> loadData() async {
    print("loadData start");
    Response response = await getNetData(); //位置 1
    setState(() {
      widgets = json.decode(response.body); //位置 2
    });
    return response.body;
  }
  
  Future<Response> getNetData() async {
    String requestURL = 'https://hy.sohu.com/testGetStringList1';
    Client client = Client(); //位置 3
    
    Future<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{ //声明异步方法 使用 async
  print_cjf('fun2 start');
  await Future.delayed(Duration(seconds: 2)); //使用 await,会异步等待结果返回
  print_cjf('fun2 end');
}

void fun3(){
  print_cjf('fun3');
}

void fun4() async{ //声明异步方法 使用 async
  print_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 {
  ..........
  
  @override
  void 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 = 123
  console.log('cjf--- fun1---2')
  
  var b = await fun2() //等待 fun2 的异步结果
  console.log('cjf--- fun1---3')
  
  var c = b
  console.log('cjf--- fun1---4')
  return 1;
}

async function fun2() {
  console.log('cjf--- fun2---1')
  
  var a = 123
  console.log('cjf--- fun2---2')
  
  var b = await fun3() //等待 fun3 的异步结果
  console.log('cjf--- fun2---3')
  
  var c = b
  console.log('cjf--- fun2---4')
}

async function fun3() {
  console.log('cjf--- fun3---1')
  
  var a = 123
  console.log('cjf--- fun3---2')
  
  var b = await makeRequest()  //等待网络请求的异步结果
  console.log('cjf--- fun3---3')
  
  var c = b
  console.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.coloradmin.cn/o/805245.html

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

相关文章

新一代网络安全防护体系的五个关键特征

目前&#xff0c;网络安全技术正面临着一个转折点&#xff0c;基于边界的安全防护理论存在缺陷&#xff0c;基于规则的威胁判别机制不再有效&#xff0c;围绕传统技术构建的安全工程也不再适用。新一代安全建设不能再像修“城墙”一样&#xff0c;专注于外部网络攻击和已知威胁…

C++:类和对象(中)---默认成员函数---运算符重载---const的含义

文章目录 默认成员函数构造函数析构函数拷贝构造函数运算符重载赋值运算符重载const的含义取地址及const取地址操作符重载 默认成员函数 首先要理解什么是默认成员函数&#xff1a;类在什么都不写的时&#xff0c;编译器会生成六个默认成员函数 用户没有显式实现&#xff0c;但…

谁能讲清楚Spark之小白入门

在这我假设大家都是小白&#xff0c;那么Spark是什么&#xff1f;你为什么搜索它&#xff1f;思考一下。 首先&#xff0c;Spark是大数据处理框架的一种&#xff0c;那么什么是大数据处理框架&#xff1f;什么是大数据&#xff1f;字面意思懂得都懂。&#xff08;如果不懂去百度…

网络安全代码合集

SQL注入 联合注入 ?id1and 11-- - ?id1order by 1-- - ?id-1union select 1,2,3-- - ?id-1union select 1,database(),3-- - ?id-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase() -- - ?id-1 union sele…

Docker啥是容器编排?

文章目录 容器编排compose案例wordpress案例swarmKubernetes特点 容器编排 Docker容器编排是一种管理和协调多个Docker容器的技术&#xff0c;旨在简化容器化应用程序的部署、扩展和管理。在现代应用开发中&#xff0c;容器化已经成为一种流行的部署方式&#xff0c;Docker是其…

A Deep Framework for Hyperspectral Image Fusion Between Different Satellites

1.摘要 最近&#xff0c;将低分辨率高光谱图像&#xff08;LR-HSI&#xff09;与不同卫星的高分辨率多光谱图像&#xff08;HR-MSI&#xff09;融合已成为提高HSI分辨率的有效方法。然而&#xff0c;由于不同的成像卫星、不同的照明条件和相邻的成像时间&#xff0c;LR-HSI和H…

15.Netty源码之EventLoop

highlight: arduino-light Netty配置主从Reactor模式 通过将NioServerSocketChannel绑定到了bossGroup。 将NioServerSocketChannel接收到请求创建的SocketChannel放入workerGroup。 将2个不同的SocketChannel绑定到2个不同的Group完成了主从 Reactor 模式。 分配NIOEventLoop的…

【Git|项目管理】Git的安装以及本地仓库的创建和配置

文章目录 1.Git简介2.安装Git2.1在Centos上安装git2.2 在ubuntu上安装git 3.创建本地仓库4.配置本地仓库 1.Git简介 Git是一个分布式版本控制系统&#xff0c;用于跟踪和管理文件的更改。它可以记录和存储代码的所有历史版本&#xff0c;并可以方便地进行分支管理、合并代码和协…

JavaScript中的this指向及绑定规则

在JavaScript中&#xff0c;this是一个特殊的关键字&#xff0c;用于表示函数执行的上下文对象&#xff0c;也就是当前函数被调用时所在的对象。由于JavaScript的函数调用方式多种多样&#xff0c;this的指向也因此而变化。本文将介绍JavaScript中this的指向及绑定规则&#xf…

【LeetCode】141.环形链表

题目 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#…

M.2 接口

M.2 接口 简介 M.2模块卡口类型 常用硬盘接口 • B key&#xff1a;传输模式为PCI-E 2X或SATA&#xff0c;用于SSD或WWAN • M Key&#xff1a;传输模式为PCI-E 4X或SATA&#xff0c;传输速率达到4GB/s&#xff0c;应用于NVMe PCIe M.2 SSD等。 接口类型 常用硬盘尺寸 M.2设…

sed命令替换 M-BM- 不可见字符 解决脚本或配置文件粘贴后莫名其妙的报错

在使用shell脚本、编辑配置文件时&#xff0c;如果是直接复制&#xff0c;有时会莫名其妙报错。 使用cat -A查看文件&#xff0c;可以看到非常多的 M-BM- 不可见字符 挤占在空格位&#xff0c;正是这些字符导致脚本或配置文件的读取出错、 使用sed命令将文件内的 M-BM- 不可见…

C# IO 相关功能整合

目录 删除文件和删除文件夹 拷贝文件到另一个目录 保存Json文件和读取Json文件 写入和读取TXT文件 打开一个弹框&#xff0c;选择 文件/文件夹&#xff0c;并获取路径 获取项目的Debug目录路径 获取一个目录下的所有文件集合 获取文件全路径、目录、扩展名、文件名称 …

原生js vue react通用的递归函数

&#x1f642;博主&#xff1a;锅盖哒 &#x1f642;文章核心&#xff1a;原生js vue react通用的递归函数 目录大纲 1.递归函数的由来 2.代码逻辑 1.递归函数的由来 递归函数的由来可以追溯到数学中的递归概念和数学归纳法。 在数学中&#xff0c;递归是指通过定义基本情况和…

windows切换php版本以及composer

前提 安装php8.2 安装Php7.4 下载 nts是非线程安全的&#xff0c;这里选择线程安全的&#xff0c;选择64位 解压缩 修改系统环境变量 修改为php-7的 cmd中输入php -v查看 找到composer存放路径C:\ProgramData\ComposerSetup\bin 将三个文件复制到php目录下 重启电脑…

flink采用thrift读取tablets一个天坑

原先的配置 [INFO] StarRocksSourceBeReader [open Scan params.mem_limit 8589934592 B] [INFO] StarRocksSourceBeReader [open Scan params.query-timeout-s 600 s] [INFO] StarRocksSourceBeReader [open Scan params.keep-alive-min 100 min] [INFO] StarRocksSourceBeRea…

中间件安全-CVE漏洞复现-Docker+Websphere+Jetty

中间件-Docker Docker容器是使用沙盒机制&#xff0c;是单独的系统&#xff0c;理论上是很安全的&#xff0c;通过利用某种手段&#xff0c;再结合执行POC或EXP&#xff0c;就可以返回一个宿主机的高权限Shell&#xff0c;并拿到宿主机的root权限&#xff0c;可以直接操作宿主机…

【图论】Prim算法

一.介绍 Prim算法是一种用于解决最小生成树问题的贪心算法。最小生成树问题是指在一个连通无向图中找到一个生成树&#xff0c;使得树中所有边的权重之和最小。 Prim算法的基本思想是从一个起始顶点开始&#xff0c;逐步扩展生成树&#xff0c;直到覆盖所有顶点。具体步骤如下…

【Ansible】

目录 一、Ansible简介二、ansible 环境安装部署1、管理端安装 ansible 三、ansible 命令行模块&#xff08;重点&#xff09;1&#xff0e;command 模块2&#xff0e;shell 模块3、cron 模块4&#xff0e;user 模块5&#xff0e;group 模块6&#xff0e;copy 模块&#xff08;重…

后台管理系统中刷新业务功能的实现

实现 由于刷新业务涉及路由通信所以在store/pinia创建全局变量refresh state:()>{return{// 是否刷新refresh:false,}},在header组件中是为刷新按钮绑定点击实现并对refresh取反操作 <el-button type"default" click"refresh!refresh" icon"R…