1)UI Task Runner Thread(Dart Runner)
UI Task Runner 用于执行 Dart root isolate 代码(isolate 我们后面会讲到,姑且先简单理解为 Dart VM 里面的线程)。Root isolate 比较特殊,它绑定了不少 Flutter 需要的函数方法,以便进行渲染相关操作。对于每一帧,引擎要做的事情有:
- Root isolate 通知 Flutter Engine 有帧需要渲染。
- Flutter Engine 通知平台,需要在下一个 vsync 的时候得到通知。
- 平台等待下一个 vsync。
- 对创建的对象和 Widgets 进行 Layout 并生成一个 Layer Tree,这个 Tree 马上被提交给 Flutter Engine。当前阶段没有进行任何光栅化,这个步骤仅是生成了对需要绘制内容的描述。
- 创建或者更新 Tree,这个 Tree 包含了用于屏幕上显示 Widgets 的语义信息。这个东西主要用于平台相关的辅助 Accessibility 元素的配置和渲染。
2)GPU Task Runner
GPU Task Runner 主要用于执行设备 GPU 的指令。UI Task Runner 创建的 Layer Tree 是跨平台的,它不关心到底由谁来完成绘制。GPU Task Runner 负责将 Layer Tree 提供的信息转化为平台可执行的 GPU 指令。GPU Task Runner 同时负责绘制所需要的 GPU 资源的管理。资源主要包括平台 Framebuffer,Surface,Texture和Buffers 等。
一般来说 UI Runner 和 GPU Runner 跑在不同的线程。GPU Runner 会根据目前帧执行的进度去向 UI Runner 要求下一帧的数据,在任务繁重的时候可能会告诉 UI Runner 延迟任务。这种调度机制确保 GPU Runner 不至于过载,同时也避免了 UI Runner 不必要的消耗。
建议为每一个 Engine 实例都新建一个专用的 GPU Runner 线程。
3)IO Task Runner
前面讨论的几个 Runner 对于执行流畅度有比较高的要求。Platform Runner 过载可能导致系统 WatchDog 强杀,UI 和 GPU Runner 过载则可能导致 Flutter 应用的卡顿。但是 GPU 线程的一些必要操作,例如 IO,放到哪里执行呢?答案正是 IO Runner。
IO Runner 的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为 GPU Runner 的渲染做好准备。IO Runner 首先要读取压缩的图片二进制数据(比如 PNG,JPEG),将其解压转换成 GPU 能够处理的格式然后将数据上传到 GPU。
获取诸如 ui.Image 这样的资源只有通过 async call 去调用,当调用发生的时候 Flutter Framework 告诉 IO Runner 进行加载的异步操作。
IO Runner 直接决定了图片和其它一些资源加载的延迟间接影响性能。所以建议为 IO Runner 创建一个专用的线程。
4)Platform Task Runner
Flutter Engine 的主 Task Runner,类似于 Android Main Thread 或者 iOS 的 Main Thread。但是需要注意他们还是有区别的。
一般来说,一个 Flutter 应用启动的时候会创建一个 Engine 实例,Engine创建的时候会创建一个线程供 Platform Runner 使用。
跟 Flutter Engine 的所有交互(接口调用)必须在 Platform Thread 进行,否则可能导致无法预期的异常。这跟 iOS UI 相关的操作都必须在主线程进行相类似。需要注意的是在 Flutter Engine 中有很多模块都是非线程安全的。
阻塞 Platform Thread 不会直接导致 Flutter 应用的卡顿(跟 iOS Android 主线程不同)。尽管如此,也不建议在这个 Runner 执行繁重的操作,长时间卡住 Platform Thread 应用有可能会被系统 Watchdog 强杀。
各个平台的线程配置
1.iOS为每个引擎实例的UI,GPU和IO任务运行程序创建专用线程。所有引擎实例共享相同的Platform Thread和Platform Task Runner。2.Android为每个引擎实例的UI,GPU和IO任务运行程序创建专用线程。所有引擎实例共享相同的Platform Thread和Platform Task Runner。3.Fuchsia每一个Engine实例都为UI,GPU,IO,Platform Runner创建各自新的线程。4.Flutter Tester (used by flutter test)UI,GPU,IO和Platform任务运行器使用相同的主线程。
创建 isolate:使用 spawnUri
spawnUri 方法有三个必须的参数
- 第一个是 Uri,指定一个新 Isolate 代码文件的路径;
- 第二个是参数列表,类型是List;
- 第三个是动态消息。
需要注意,用于运行新 Isolate 的代码文件中,必须包含一个 main 函数,它是新 Isolate 的入口方法,该 main 函数中的 args 参数列表, 正对应 spawnUri 中的第二个参数。如不需要向新 Isolate 中传参数,该参数可传空 List 主 Isolate 中的代码:
import 'dart:isolate'; void main() { print("main isolate start"); create_isolate(); print("main isolate stop"); } // 创建一个新的 isolate create_isolate() async { ReceivePort rp = ReceivePort(); ///创建发送端 SendPort port1 = rp.sendPort; ///创建新的 isolate ,并传递了发送端口。 Isolate newIsolate = await Isolate.spawnUri(Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], port1); SendPort port2; rp.listen((message){ print("main isolate message: $message"); if (message[0] == 0){ port2 = message[1]; }else{ port2?.send([1,"这条信息是 main isolate 发送的"]); } }); // 可以在适当的时候,调用以下方法杀死创建的 isolate // newIsolate.kill(priority: Isolate.immediate); }
在主 isolate 文件的同级路径下,新建一个 other_task.dart ,内容如下:
import 'dart:isolate'; import 'dart:io'; ///接收到了发送端口。 void main(args, SendPort port1) { print("isolate_1 start"); print("isolate_1 args: $args"); ReceivePort receivePort = ReceivePort(); SendPort port2 = receivePort.sendPort; receivePort.listen((message){ print("isolate_1 message: $message"); }); // 将当前 isolate 中创建的SendPort发送到主 isolate中用于通信 port1.send([0, port2]); // 模拟耗时5秒 sleep(Duration(seconds:5)); port1.send([1, "isolate_1 任务完成"]); print("isolate_1 stop"); }
运行主 isolate,输出结果:
main isolate start main isolate stop isolate_1 start isolate_1 args: [hello, isolate, this is args] main isolate message: [0, SendPort] isolate_1 stop main isolate message: [1, isolate_1 任务完成] isolate_1 message: [1, 这条信息是 main isolate 发送的] 复制代码
isolate 之间的通信时通过 ReceivePort 来完成的,ReceivePort 可以理解为一根水管,消息的传递方向时固定的,把这个水管的发送端发送给对方, 对方就能通过这个水管发送消息。
创建 isolate:通过 spawn
除了使用 spawnUri,更常用的是使用 spawn 方法来创建新的 Isolate,我们通常希望将新创建的 Isolate 代码和 main Isolate 代码写在同一个文件, 且不希望出现两个 main 函数,而是将指定的耗时函数运行在新的 Isolate,这样做有利于代码的组织和代码的复用。 spawn 方法有两个必须的参数
- 第一个是需要运行在新 Isolate 的耗时函数
- 第二个是动态消息,该参数通常用于传送主 Isolate 的 SendPort 对象
- (可选)第三个是命名方法,这个值可以做表意使用,但他不是唯一的
import 'dart:isolate'; import 'dart:io'; void main() { print("主程序开始"); create_isolate(); print("主程序结束"); } // 创建一个新的 isolate create_isolate() async{ ReceivePort rp = new ReceivePort(); SendPort port1 = rp.sendPort; Isolate newIsolate = await Isolate.spawn(doWork, port1); SendPort port2; rp.listen((message){ print("main isolate message: $message"); if (message[0] == 0){ port2 = message[1]; }else{ port2?.send([1,"这条信息是 main isolate 发送的"]); } }); } // 处理耗时任务 void doWork(SendPort port1){ print("new isolate start"); ReceivePort rp2 = new ReceivePort(); SendPort port2 = rp2.sendPort; rp2.listen((message){ print("doWork message: $message"); }); // 将新isolate中创建的SendPort发送到主isolate中用于通信 port1.send([0, port2]); // 模拟耗时5秒 sleep(Duration(seconds:5)); port1.send([1, "doWork 任务完成"]); print("new isolate end"); }
结果:
主程序开始 主程序结束 new isolate start main isolate message: [0, SendPort] new isolate end main isolate message: [1, doWork 任务完成] doWork message: [1, 这条信息是 main isolate 发送的]
创建 isolate:Compute 函数
Compute 函数对 isolate 的创建和底层的消息传递进行了封装,使得我们不必关系底层的实现,只需要关注功能实现。
首先我们需要:
- 一个函数:必须是顶级函数或静态函数
- 一个参数:这个参数是上面的函数定义入参(函数没有参数的话就没有)
- (可选)debugLabel:字典类参数,命名线程
比如计算斐波那契数列:
void main() async{ // 调用compute函数,compute函数的参数就是想要在isolate里运行的函数,和这个函数需要的参数 print(await compute(syncFibonacci, 20)); runApp(MyApp()); } int syncFibonacci(int n){ return n < 2 ? n : syncFibonacci(n-2) + syncFibonacci(n-1); }
运行后的结果如下:
flutter: 6765