详解 Flutter engine多线程、Dart isolate和异步

1. 前言

随着Flutter的使用越来越广泛,相信很多人包括我自己对flutter的线程一直存一些疑问, dart为什么默认是单线程任务处理、在单线程下dart的异步是如何实现的、flutter线程有哪些、如何使用多线程处理耗时操作... 带着这些疑问去探索下flutter engine多线程、 dart isolate 和 异步 三者之前的关系。

2. Flutter 的线程

首先介绍下Flutter engine 的多线程,从源码可以看到有五种线程,platform、UI、raster、io、profiler,profiler 线程 只有在profile Mode才会开启。

ThreadHost::ThreadHost(std::string name_prefix_arg, uint64_t mask)
    : name_prefix(name_prefix_arg) {
  if (mask & ThreadHost::Type::Platform) {
    platform_thread = std::make_unique<fml::Thread>(name_prefix + ".platform");
  }

  if (mask & ThreadHost::Type::UI) {
    ui_thread = std::make_unique<fml::Thread>(name_prefix + ".ui");
  }

  if (mask & ThreadHost::Type::RASTER) {
    raster_thread = std::make_unique<fml::Thread>(name_prefix + ".raster");
  }

  if (mask & ThreadHost::Type::IO) {
    io_thread = std::make_unique<fml::Thread>(name_prefix + ".io");
  }

  if (mask & ThreadHost::Type::Profiler) {
    profiler_thread = std::make_unique<fml::Thread>(name_prefix + ".profiler");
  }
}

TaskRunner

官方对flutter engine 多线程做了详细介绍,做了如下摘要:

Flutter engine 不创建或管理自己的线程。而是由中间层embeder为Flutter引擎创建和管理线程(以及它们的消息循环)。embeder给Flutter engine 提供它所管理的线程的Task Runner。除了embeder为engine管理的线程外,Dart VM也有自己的线程池。无论是Flutter egnine还是embeder都无法访问该池中的线程。

  • 「 PlartForm Task Runer 」

PlartForm Task Runer对应的线程就是主线程,类似于 Android 的 MainThread 以及 iOS 的 UIKit 文档。

  • 必须在主线程(PlartForm Task Runer)调用 flutter engine 的API
  • PlartForm Task Runer 也会响应其他线程消息来调用 engine 的API

尽管阻塞平台线程的时间过长不会阻塞Flutter的渲染,但平台确实对这个线程上的耗时操作施加了限制。因此,官方建议在响应平台消息时,任何昂贵的工作都应该在单独的工作线程(与上面讨论的四个线程无关)上执行,然后再将响应排回到平台线程上,提交给引擎。不这样做可能会导致平台特定的看门狗终止应用程序。Android和iOS等嵌入系统也使用平台线程来处理用户输入事件。一个阻塞的平台线程也会导致手势被丢弃。

  • UI Task Runner UI Task Runner是engine为root isolate 执行所有Dart代码的地方(root isolate 运行在Dart VM并非UI线程),包括开发者写下的代码和 Flutter框架根据应用行为生成的代码。Root isolate比较特殊,它关联了很多Flutter的函数方法,engine为root isolate上设置了绑定,使其能调度和提交帧。对于Flutter需要渲染的每一帧,root isolate 必须通知engine
    • root isolate会通知engine有帧需要渲染。
    • engine 会询问平台是否应该在下一个vsync通知它。
    • 平台会等待下一个vsync的到来。
    • 响应vsync,将唤醒Dart代码并执行以下工作:
      • 更新动画插值器(控制动画速度)
      • 重新build 应用程序在build阶段的widget
      • layout新构建的widget,将它们绘制成layertree提交给engine,没有进行光栅化,仅仅是绘制阶段需要被绘制的内容的描述
      • 创建或者更新Tree,这个Tree包含了用于屏幕上显示Widgets的语义信息。这个东西主要用于平台相关的辅助Accessibility元素的配置和渲染。

除了为engine构建最终渲染的框架外,root isolate 需要执行、计时器、microtaskQueue 和异步I/O 的所有响应

由于UI线程构建了决定引擎最终会在屏幕上绘制什么的图层树,所以屏幕上所有能看到的UI都来源于这里,不能阻塞该线程

  • Raster Task Runner GPU Task Runner被用于执行需要访问GPU的任务,UI线程创建的layer tree 是不区分平台的,不同平台采用的绘制实现是不一样的,可以是OpenGL,Vulkan,软件绘制或者其他Skia配置的绘图实现。
    • Raster Task Runner将layer tree的信息构建出适当的GPU命令
    • GPU Task Runner同时也负责为每一帧绘制配置所需要的GPU资源:
      • 平台Framebuffer的创建
      • Surface生命周期管理
      • 保证Texture和Buffers在绘制的时候是可用的。

根据处理图层树和设备完成显示帧所需的时间,Raster Task runner 的各个组件可能会延迟UI线程上的进一步帧的调度。通常情况下,UI和Raster Task runner 是在不同的线程上。在这种情况下,Raster线程可能正在向GPU提交一帧,而UI线程已经在准备下一帧。这种管道机制确保了UI线程不会为Raster Task runner 安排过多的任务。

这个线程之前被叫做「GPU 线程」,因为它为 GPU 进行栅格化,但我们重新将它命名为「raster 线程」,这是因为许多开发者错误的(但是能理解)认为该线程运行在 GPU 单元。

  • I/O Task Runner 」

到目前为止,所有提到的Task Runner都对可以在这上面执行的各种操作有相当强的限制。阻断Platform Task Runner 的时间过长可能会触发平台的看门狗,而阻断UI或Raster Task Runner 都会导致Flutter应用程序的卡顿。 Raster有一些必要的任务,需要做一些非常耗时的工作。这种耗时的工作是在IO任务运行器上进行的,

  • 只有 raster 可以访问 GPU,给 GPU 下发命令
  • I/O 和 Raster 两个TaskRunner 都有context,并且在同一个shareGroup

比如 I/O线程 从存储中读取图片解码后将数据交给Raster线程处理最终交接给GPU,从而减轻了Raster线程的压力;

  • Profiler线程 」

通过查看 Flutter engine 源码,开启profiler模式会启动profiler_thread 获取对应的 TaskRunner(),有点类似于开辟常驻线程,检测卡顿性能。官方推荐我们在profile mode下监测性能。

- (void)startProfiler {
  FML_DCHECK(!_threadHost->name_prefix.empty());
  _profiler_metrics = std::make_shared<flutter::ProfilerMetricsIOS>();
  _profiler = std::make_shared<flutter::SamplingProfiler>(
      _threadHost->name_prefix.c_str(), _threadHost->profiler_thread->GetTaskRunner(),
      [self]() { return self->_profiler_metrics->GenerateSample(); }, kNumProfilerSamplesPerSec);
  _profiler->Start();
}

 3. Dart Isolate

Isolate class

An isolated Dart execution context.

All Dart code runs in an isolate, and code can access classes and values only from the same isolate. Different isolates can communicate by sending values through ports (see ReceivePort, SendPort).

官方isolate的定义, Isolate 是一个隔离的Dart执行上下文 」dart代码都运行在isolate, 代码只能在同一个isolate获取类和值,不同的isolate只能通过发送port和接受port进行消息传递

不同于其他平台,例如 iOS多个线程可以对一个对象做读写操作,需要「添加锁」使其遵循「多读单写」的原则

  • Dart的线程是 isolate 形式存在的,不同线程的内存是互相隔离的,没有共享的内存,不会造成资源竞争,也不需要使用锁,更不会有死锁以及锁造成的性能消耗等问题

  •  大多数的flutter App 运行在单独的isolate上,如果复杂的计算或者比较重的耗时操作(百毫秒以上)造成UI卡顿,我们也可以使用 Isolate.spawn() or Flutter’s compute() function. 创建新的isolate来执行异步耗时任务,实现任务并发
  • 每个isolate 都有自己独立的内存空间、eventloop、eventQueue、MicrotaskQueue 如下图

   

  • isolate 可以通过sendPort 以及 receivePort 以stream的方式 互相传递信息,receivePort继承stream

isolate 创建以及isolate 之前的消息传递

  • 通过isolate.spawn 创建childisolate, 并创建receivePort r1并将它生成的sendport s1 传递给childisolate;
  • childisolate 也创建自己的receivePort r2, 通过接受到的 r1向主线程发送r2生成的sendport s2, 这样两个isolate 两个isoate都有了对方的sendport
  • main isolate 创建新的recevePort, 并通过获取到的s2 向childisolate 发送消息并传递新的sendport replyport,
  • 最终childisolate 接收到消息,通过replyport 发送给main isolate,双向通信实现

过程有点类似于socket 三次握手

import 'dart:isolate';

void main(List<String> args) async {
  print("current thread ${Isolate.current.debugName}");
  ReceivePort r1 = ReceivePort();
  SendPort s1 = r1.sendPort;
  Isolate.spawn(childIsolate, s1);
  SendPort s2 = await r1.first;
  var data = await sendtoReceive(s2, "Hi 我是主线程");
  print("主线程收到消息:$data");
}

Future sendtoReceive(SendPort port, msg) {
  ReceivePort reponse = ReceivePort();
  port.send([reponse.sendPort, msg]);
  return reponse.first;
}

// 创建新的isolate
void childIsolate(SendPort s1) async {
  print("current thread ${Isolate.current.debugName}");
  ReceivePort r2 = ReceivePort();
  SendPort s2 = r2.sendPort;
  s1.send(s2);
  var data = await r2.first;

  SendPort replyPort = data[0];
  print("子线程收到: ${data[1]}");
  replyPort.send("Hello this is child isolate");

最终打印结果

current thread main
current thread childIsolate

子线程收到: Hi 我是主线程
Hello this is child isolate

isolate 还有pause、resume、kill 等api 可以动手尝试。

compute

compute 方式实际上是对isolate 一系列方法的封装,类似于iOS dispatch_async 和 NSOperationQueue的关系。

 await compute(function())

function() 为耗时方法, compute 对返回值的类型是有要求的。

Flutter Engine thread 和 Dart Isolate 的关系

当启动一个flutter程序后,做完一些初始化操作后,会创建上述四个taskRunner 以及对应的线程,创建DartVM 和engine,然后会创建Root Isolate运行在DartVM。

DartVM拥有自己线程池(isolate), Flutter engine 和 embeder 都不能直接访问. isolate的生命周期完全由dartVM管理。

上文提到UI线程为root isolate 执行所有的 Dart 代码, 原因在于 root isolate 会通过task_dispatch发送消息,将任务提交给UI Task Runner. 通过 isolate.spawn创建的isolate 没有这个权限。

Engine thread 和 Dart Isolate 通过互相发送消息来执行dart代码,Engine thread 由embeder 创建管理,Isolate由DartVM管理 两者是相互独立,彼此协作的关系。

 Flutter是依赖于Dart语言的跨平台框架,使用dart语言就需要模拟dart运行环境,创建DartVM,DartVM中 Root isolate 有且仅有一个 所以 Dart默认是单线程任务处理,使用dart语言的Flutter的App 默认也是单线程任务处理,如果需要实现并发,需要新建isolate

4. 异步

谈到异步首先要介绍EventLoop和Future, 这是因为有了 EventLoop和Future 使 Dart 可以异步执行任务

Event Loop

Dart 一次只能执行一个任务,任务按照顺序一个接一个的执行,不能被其他任务打断。那 dart 是如何实现异步操作的呢?

Isolate 会维护 EventLoop, 执行完Main()中的任务后,EventLoop会循环执行两个队列的任务 MicrotaskQueue、eventQueue。

  • MicrotaskQueue、eventQueue 任务都遵循 FIFO 规则(Firtst in First out)
  • MicrotaskQueue 优先级要高于eventQueue, 只有MicrotaskQueue中task执行完以后才会执行eventQueue 的task

Future

future 是 Future<T> 类的对象,其表示一个 T 类型的异步操作结果。如果异步操作不需要结果,则 future 的类型可为 Future<void>。当一个返回 future 对象的函数被调用时,会发生两件事:

  • 将函数操作列入队列等待执行并返回一个未完成的 Future 对象。
  • 不久后当函数操作执行完成,Future 对象变为完成并携带一个值或一个错误。

当你写的代码依赖于 future 对象时,你有两种可选的实现方式:

  • 使用关键字 asyncawait
  • 使用 Future API

async await 被称作语法糖,下面的解释用于形容语法糖很恰当。

调用顺序

使用Future 可以把实现异步操作,但是哪些任务会被直接执行,哪些任务会放到eventQueue,哪些任务被放到MicrotakQueue,通过代码调试可以得出总结出。

void _futuretest() async {
    Future.delayed(Duration(seconds: 1), () => print("Event Queue 3"));
    Future(() => {print("Event Queue 1")}).then((value) => print("then 3"));
    Future.delayed(Duration.zero, () => print("Event Queue 2")).then((value) {
      scheduleMicrotask(() => print("Microtask 6"));
      print("then 2");
    }).then((value) {
      print("then 1");
    });

    scheduleMicrotask(() => {print("Microtask 1")});
    Future.microtask(() => print("Microtask 2"))
        .then((value) => print("then 4"));    
    Future.value().then((value) => print("Microtask 3"));

    print("main 1");
    Future.sync(() => print("vsync 2")).then((value) => print("Microtask 4"));
    Future.value(getValue()).then((value) => print("Microtask 5"));
    print("main 4");
  }

  String getValue() {
    print("value 3");
    return "value 3";
  }
// 打印结果:
flutter: main 1
flutter: vsync 2
flutter: value 3
flutter: main 4

flutter: Microtask 1
flutter: Microtask 2
flutter: then 4
flutter: Microtask 3
flutter: Microtask 4

flutter: Event Queue 1
flutter: then 3
flutter: Event Queue 2
flutter: then 2
flutter: then 1
flutter: Microtask 6
flutter: Event Queue 3
flutter: Event Queue 4

直接调用

  • Future.sync()
  • Future.value()
  • (Microtask和event task).then()

Microtask

  • scheduleMicrotask()
  • Future.microtask()
  • (sync value).then()

Eventtask

  • Future()
  • Future.delayed()

Future 和 Isolate

使用future 仅仅只是改变了任务的调用顺序,任务仍然是串行的,而新建isolate 才是真正实现了并发;Future 和 Isolate 类似于异步和并发的关系

实际开发中Future 能满足我们大多数的使用需求,毕竟isolate的创建和销毁也会耗时,只有一些耗时操作(百毫秒级别)建议使用compute,具体还要根据实际开发需求,选择合适的方案

  • 高清大图的加载、图片裁剪、美颜...
  • 音视频裁剪、格式转换
  • 文件数据读取、解析,加密解密

5. 小结

本文主要探讨了Flutter engine 多线程机制 以及 dart isolate 实现原理。 相信经过上述分析,大家对flutter和dart线程有了更深入的理解,如有纰漏还望指出,希望大家多多支持。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值