[译] Flutter - wiki - 引擎架构

1,162 阅读10分钟

日期: 2020-08-08
原文: github.com/flutter/flu…

引擎架构

Flutter 是由一个 Dart 框架和高性能引擎组成的.

Flutter 引擎是一个为高质量移动应用打造的便携运行时环境.它实现了 Flutter 核心库,包含动画和图像,文件和网络I/O,辅助功能支持,插件架构,和 Dart 运行时及辅助开发、编译、运行 Flutter 应用的工具链.

架构预览

Flutter 引擎集成核心技术: Skia, 一个 2D 图形渲染库、 Dart, 具有垃圾回收功能的面向对象语言的虚拟机、然后把他们托管在 shell 中.不同的平台有不同的 shell,如 AndroidiOS.同样也有 embedder API 可以让 Flutter 引擎作为库来使用(查看自定义Flutter 引擎 Embedder).

shell 实现了平台独立的代码如和 IMEs 交互(屏幕上的键盘),系统应用的生命周期事件等.

Dart 虚拟机实现了基本的 Dart 核心库,再加上提供了访问 Skia 特性和 shell 的底层接口的 dart:ui 库.shell 也可以通过 Platform Channels 直接和 Dart 代码交互而绕过引擎.

线程

预览

Flutter 引擎不会去创建或者管理自己的线程.相反,为 Flutter 引擎创建和管理线程(及对应的消息循环),这是 embedder 的职责.embedder 向 Flutter 引擎的 task runner 提供它自己管理的线程.除了 embedder 为引擎管理的线程外, Dart 虚拟机也拥有自己的线程池.Flutter 引擎和 embedder 都不能访问 Dart 虚拟机中线程池的线程.

Task Runner 配置

Flutter 引擎会要求 embedder 将其引用给 4 个 task runner.引擎不管这个引用是不是会绑定到一个相同的 task runner,或者多个 task runner 运行在同一个线程上.为了获得最佳性能,embedder 应该为每个 task runner 创建一个单独的线程.尽管引擎不关心线程正在运行的是哪个 task runner,但是它希望在整个引擎生命周期中线程的配置应该保持稳定.也就是说,一旦 embedder 决定某个线程运行某个 task runner,那么它就应该只在这个指定线程运行指定 task runner 来执行任务(直到引擎关闭).

主要的 task runner 如下:

  • Platform Task Runner
  • UI Task Runner
  • GPU Task Runner
  • IO Task Runner
Platform Task Runner

这个 task runner 绑定的线程应该被 embedder 视为主线程.例如,对安卓来说是 Android Main Thread 或对 Apple 平台来说被 Foundation 引用的 Main Thead.

embedder 完全负责为此 task runner 分配线程.Flutter 引擎的分配对此线程来说没有任何意义.实际上,多个 Flutter 引擎可以通过在基于不同线程的 platform task runner 同时运行.这也是 Flutter Content Handler 为什么能在 Fuchsia 工作的原因.每个 Flutter 应用都会在其进程中创建一个新 Flutter 引擎,而每个引擎都会创建一个新 platform 线程.

和 Flutter 引擎的交互必须发生在 platform 线程上.在其他线程和引擎交互将会陷入未优化的构建断言,在正式版构建中也是线程不安全的.同时,Flutter 引擎中有大量的组件都是线程不安全的.一旦引擎配置启动运行,只要 embedder API 的访问是发生在 platform 线程上,embedder 就不必向配置引擎的任何 task runner 发布任务了.

除了引擎启动后成为 embedder 和引擎交互的指定线程外,此 task runner 也会指定任意待处理的平台消息.这是因为访问大多数 platform API只有在 platform 的主线程才是安全的.而插件是不必把他们的调用重新切换到主线程的.如果插件管理着自己的 worker 线程,那么在将响应提交到引擎由 Dart 代码处理前,应该由插件自己在 platform 线程对响应排列.所有和引擎的交互都发生在 platform 线程的原则就体现在这.

即使阻塞 platform 过多时间也不会阻塞 Flutter 的渲染管道, platform 会对这个线程的耗时操作采取限制措施.所以在把 platform 线程处理的响应队列提交到引擎之前,建议所有响应平台消息的耗时操作都在独立的工作线程(除了上述的4种线程外的其他线程)执行.不这样的话就可能导致平台独立的 watchdog 终结此应用.像 Android 和 iOS 这样的 embedder 也使用 platform 线程通过管道传递他们的输入事件.阻塞了 platform 线程同样可能会导致手势事件被丢弃.

UI Task Runner

Ui Task Runner 就是引擎在其 root isolate 执行所有 Dart 代码的地方. root isolate 是一个特殊的 isolate,具有使 flutter 生效的必要的 binding.此 isolate 运行应用的 main Dart 代码.binding 使引擎在此 isolate 设置并安排提交帧数据.下面是 Flutter 必须渲染的每一帧:

  • root isolate 告诉引擎这帧需要被渲染.
  • 引擎询问 platform 它在下次 vsync 是否被通知.
  • platform 等待下次 vsync.
  • vsync 到来时,引擎将唤醒 Dart 代码执行下列任务:
    • 更新动画插值器.
    • 在 build 阶段重建应用的 widgets.
    • 摆放新的构建、组件,把它们画到 layer tree上然后立即提交到引擎.此处不会发生栅格化操作;仅仅是将在绘制阶段的一个需要被绘制结构的描述而已.
    • 构造或更新关于屏幕上组件的语义信息的节点树.此树被用来更新平台特定的辅助组件.

除了为引擎构建最终需要渲染的帧外,root isolate 同样执行所有平台插件信息、计时器、微任务和异步 I/O(来自 sockets, 文件句柄等)的响应.

因为 UI 线程构造的层级树决定了引擎最终在屏幕上绘制什么,它是屏幕上所有内容的真相之源.相应的,在此线程执行长时间的同步操作也会导致 Flutter 应用卡顿丢帧(及时几毫秒也是足够丢掉下一帧的!).长时间的操作只能是由 Dart 代码引起的,因为引擎不会再此 task runner 运行任何原生代码.因此,此 task runner(或线程)也被称为 Dart 线程.embedder 是有可能向此 task runner 上安排任务的.这将导致 Flutter 卡顿丢帧, embedder 建议不要这样做,并且为此 task runner 指定专一线程.

如果 Dart 代码不可避免的执行了耗时操作,建议这些代码移到一个独立的 Dart isloate(例如使用 compute 方法).在非 root isolate 执行的 Dart 代码运行在 Dart 虚拟机管理的线程池中的线程上.这就不会导致 Flutter 应用卡顿丢帧了.终结 root isolate 将会终结所有由此 root isolate 孵化的 isolate.此外,非 root isolate 是无法调度帧,也没有 Flutter 框架依赖的 binding.因此,你没有任何有效的办法在第二个 isolate 中和 Flutter 框架交互.使用第二个 isolate 执行那些重量级运算任务吧.

Raster Task Runner

raster task runner 执行需要访问设备 rasterizer(通常由 GPU 提供)的任务.由 Dart 代码在 UI task runner 创建的层级树是客户端渲染API不可知论者.也就是说,相同的层级树可以使用 OpenGL,Vulkan,软件或实际上为 Skia 配置的其他后端来渲染帧.GPU task runner 上的组件获取层级树,构建合适的绘制命令.raster task runner 组件同样负责为特定的某一帧配置所有的 GPU 资源.包括和平台交互配置 framebuffer,管理 surface 生命周期,确保特定某帧的 textures 和 buffers 完全准备好.

根据层级树处理花费的时间和设备完成显示帧, raster task runner 的不同组件可能会推迟 UI 线程未来其他帧的调度.通常 Ui 和 raster task runner 是运行在不同线程上的.这样当 UI 线程已经准备好下一帧的时候, raster 线程可能正在向 GPU 提交帧的过程中.这个管道机制可以确保 UI 线程不会为 rasterizer 调度过多的任务.

因为 raster task runner 组件会使 UI 线程的帧调度推迟,在 raster 线程执行过多的任务可能会导致 Flutter 应用卡顿丢帧.通常用户是没有机会在此 task runner 执行自定义任务的,因为平台代码和 Dart 代码都无法访问这个 task runner.但是 embedder 依然可能会向此线程规划任务.针对这种情况,推荐 embedder 为每个引擎实例提供一个独占专一的线程的 raster task runner.

IO Task Runner

目前提到的所有 task runner 对可以执行的任务都有很强的限制.长时间阻塞 platform task runner 会触发平台 watchdog,无论阻塞 Ui 或 raster task runner 都将会导致 Flutter 应用卡顿丢帧.然而,raster 线程会有一些耗时的操作需要完成.这个耗时操作就是在 IO task runner 上执行的.

IO task runner 的主要功能是从 asset store 读取压缩的图片,然后确保在这些图片准备好在 raster task runner 渲染.为了确保 texture 是为了渲染而读取的,首先必须从压缩数据(通常是 PNG, JPEG等)读取出来作为 blob.从 asset store,解压成 GPU 友好地格式,然后上传到 GPU.这些都是耗时操作而且如果在 raster task runner 执行可能会导致卡顿丢帧.因为只有 raster task runner 可以访问 GPU,IO task runner 组件和主 raster task runner context 共用了相同的特殊 context 即在同一个 sharegroup 里.这是在引擎配置时设置的,同样也是 IO 任务只有一个 task runner 的原因.现实中,读取压缩字节和解压缩可以在线程池中进行.但 IO task runner 是不一样的,因为访问这个 context 只有在特定线程才是安全的.像 ui.Image获取资源的唯一方式就是通过异步调用;这样框架就能和 IO task runner 交互,异步的完成上面提到的所有的 texture 操作.图像也不需要 raster 线程执行耗时操作可以立即被帧使用.

用户是无法通过 Dart 代码或原生插件访问此线程的.即使 embedder 在此线程自由的调度任务也是相当耗时的.这不会导致 Flutter 应用卡顿丢帧,但是可能推迟及时解析未来图片和资源的时机.即便如此,还是建议自定义 embedder 为此 task runner 设置专一线程.

当前平台特定的线程配置

如上所述,引擎支持多个线程配置,当前平台使用的配置如下:

iOS

每个引擎实例分别为 UI, raster 和 IO task runner 创建专一的线程.所有的引擎实例共享相同的平台线程和 task runner

Android

每个引擎实例分别为 UI, raster 和 IO task runner 创建专一的线程.所有的引擎实例共享相同的平台线程和 task runner

Fuchsia

每个引擎实例分别为 Platform, UI, raster 和 IO task runner 创建专一的线程.

Flutter Tester (由 flutter test 使用)

在进程中的单独引擎实例为 UI, raster,IO 和 Platform task runner 共享主线程.

文本渲染

当前的文本渲染栈如下:

  • 我们称为 libtxt 的 minikin derivative (字体选择,bidi,折行)
  • HarfBuzz (glyph selection, shaping)
  • SKia (渲染/GPU 后端),在 Android 和 Fuchsia 上使用 FreeType 渲染字体,iOS 上使用 CoreGraphics 渲染字体.