1 前言
在钉钉 Flutter 桌面端落地过程中,我们遇到了很多仅仅依赖 Flutter 官方文档无法解决的问题,例如:桌面端集成模式问题、内存泄露问题、卡顿问题、光标焦点异常问题等。由于无法直接通过官方文档得到答案,我们便尝试通过分析源码实现和设计文档来寻找解决办法。虽然最终大部分问题得以解决,但是在这过程中有两点一直困扰我们:
- Flutter 生态中桌面端相关资料极少。少部分官方公开资料也仅有比较宽泛的介绍,缺少详细方案设计信息;业界对 FlutterEngine 架构分析和讨论,大多也仅仅设计移动端,桌面端相关内容很少涉及;
- FlutterEngine 在移动端和桌面端 Embedder 层设计有较大差异,移动端相关资料/方案无法直接应用到桌面端。
因此我们便萌生了整理一份 Flutter 桌面端资料集的想法。一方面来作为 Flutter 桌面端设计的入门资料,降低大家上手学习桌面端引擎设计的门槛、提升效率;另外一方面也可作为工具手册,为后续我们可能逐步落地的桌面端引擎改造提供技术储备。
本文主要从宏观角度来介绍一下 FlutterEngine 桌面端设计,从发展历史、架构概述、与移动端引擎对比等角度做一下阐述,以期望读者通过本文的介绍,能对 FlutterEngine 桌面端实现方案有一个整体的了解。
2 发展脉络
通过查阅各种资料我们发现,Flutter 目标虽然是提供一套可跨多端的 UI 套件,但是 Flutter Desktop 项目发展最初并未被直接纳入 Flutter 项目。不过 Flutter Desktop 主要技术人员仍然来自 Google 团队,Flutter Desktop 项目前期独立 Flutter 主项目发展,或许是出于项目管理上的考虑。
Flutter Desktop 发展初期 git 项目为 flutter-desktop-embedding[1], 技术讨论组为Desktop Embedding for Flutter[2]。通过分析 git commit 记录以及讨论主题,我们大致可梳理出 Flutter Desktop 发展脉络时间轴:
3 架构对比
Flutter 桌面端与移动端引擎实现差异主要聚焦在两部分:
- 是否使用 Embedder API;
- 如何处理 Windows Resize。
下面针对上述两点分别介绍一下。
3.1 Embedder API
关于 Flutter 架构设计,最权威的资料当然来自官方团队提供文档(来源[3]):
注:由于 Web 平台下的实现与其它平台有较大差异、且钉钉在实际应用中并未涉及,因此在此不再分析。
无论是移动端还是桌面端,Flutter 三层结构中的 Framework(Dart) 以及 Engine(C++) 实现是共享的,实现上的主要差异聚焦在 Platform 集成上。
在梳理 Flutter Desktop 桌面端发展脉络时我们已经知道,桌面端实现整体建立在 Embedder API 基础之上(Commit[4]):
由下至上我们对上图做一下简单说明:
- Shell 即 FlutterEngine 平台无关的核心实现部分,如 Dart Runtime、Skia等;
- Embedder API是 Flutter 封装的平台无关接口层,其主要价值在于封装底层实现、简化接入流程、降低新平台接入成本;
- Android/iOS/macOS/Windows Embedder 是平台接入层,各端根据根据平台规则注入 Flutter 运行所依赖的部分,比如 渲染画布、线程管理、插件机制等;
- Android/iOS/macOS/Windows Interface是开发接口层,即 Flutter 面向不同平台暴露的使用 API。虽然不同语言暴露 API 的形式可能略有差异,但是在接口语义上四端基本一致。
3.2 Desktop Resize
在进一步分析 FlutterEngine 桌面端源码时,我们发了其相比移动端实现,除了多一层 Embedder API 以外,还有一层用于管理桌面窗口大小变化的模块:FlutterResizeSynchronizer 。
在查阅相关资料之后找到此模块相关的 设计文档:
- How to handle resize in desktop embeddings[6];
- Handling flutter view resizing on macOS[7];
- Support double buffering for window resizing[8];
在设计文档中有对此问题的说明:
简单来说即 Flutter 在异步线程渲染与 Window 在主线程变化,会导致出现 Crash、重影等一系列问题。FlutterResizeSynchronizer 的出现即为了解决此类问题。
4 实现分析
本小结我们以 macOS Embedder 为例,来分下一下 Flutter 桌面端集成部分的实现。为了方便移动端的同学更好理解,部分内容会以 iOS 端实现来做对比说明。
4.1 类图
下面这种即根据 3.1 小节的架构简图,结合 FlutterEngine macOS 端实现梳理出的核心类图:
为了便于对比理解,我们在看看 iOS 端对应的核心类图:
通过对比上两张类图,我们可以初步得到以下信息:
- 桌面端部分实现相比移动端增加一层跨平台的 Embedder API,虽然理论上可简化部分上层实现,但是因为涉及到多渲染模式、图层合成等因素的影响,桌面端整体架构复杂度并不低于移动端;
- Desktop Resize 主要由 FlutterView 来控制,在移动端端中 FlutterView 实现较薄,主要用于承载 Flutter 所需的画布;但是在桌面端其功能更为复杂,已直接参与到 Flutter 绘制流程中,并在其中起到关键作用;
- Shell 层做了较好的抽象,绝大部分场景可做到实现与平台无关;
4.2 流程对比
通过4.1小节的内容我们可知,FlutterEngine 在桌面端和移动端的差异主要聚焦在渲染绘制阶段。
根据 Flutter 官方分享资料我们知道,Flutter 渲染大致分为两个阶段:
- 第一阶段主要在 UI 线程工作,大部分由 Dart 层的 Flutter Framework 实现;
- 第二阶段在 CPU 线程工作,主要由 FlutterEngine 中的 C++ 层模块实现。
针对以上两点,第一阶段无论什么平台基本都是一致的,下面我们就结合实现大致对比一下 iOS 端与 macOS 端的第二阶段流程差异。
阶段 | iOS | macOS |
1 | DoDraw | DoDraw |
2 | DrawToSurface | DrawToSurface |
3 | AcquireFrame | AcquireFrame |
4 | Submit | Submit |
5 | Present | Try Present(窗口变化中则同步等待) |
6 | Flush | DoFlush |
注意:上述流程对比仅供示意说明问题,并非严谨流程图,有很多细节表格中并未体现
阶段 5~6 所示即 3.2 小节讨论到的「Desktop Resize」控制模块,结合 4.1 中的类图,渲染链路最终会沿着红线所表示的链路回到 FlutterView 模块,最终由 FlutterView + FlutterResizeSynchronizer + FlutterResizableBackingStoreProvider + FlutterSurfaceManager 相互配合,完成安全可靠的绘制:
移动端应为不涉及窗口大小变化,因为并无此流程。
通过上述对比我们可知,Flutter 桌面端相比移动端实现,渲染流程主要变化在于增加窗口大小变化管理模块,窗口大小变化和 Flutter 页面渲染存在同步影响:
- 如果在渲染过程中窗口发生变化,则变化动作需要等待渲染流程结束之后才可响应:可能导致 Native 主进程卡顿;
- 如果 Flutter 渲染过程中存在窗口变化,则会在窗口变化结束之后才会响应渲染:可能影响 Flutter 侧动画效果等;
在钉钉桌面端落地过程中,我们即处理过因为上述同步规则导致的 Flutter 页面创建阶段卡顿,最终通过尽量避免 Window 变化的方式来绕过。
5 小结
本文主要梳理了一下 FluttterEngine 桌面端发展脉络,并针对 FlutterEngine 桌面实现核心内容做了梳理。
通过第3和第4两个小节的分析我们可以看到,Flutter Desktop 最初基于 Embedder API 来实现,或许是期望能够通过通用的 Embedder API 来降低接入成本;并且我们通过查阅 flutter-desktop-embedding[9]commit 记录,最初基于 Embedder API 确实可以做到只通过极少的几个文件,即完成 Flutter Mac 端运行的效果:
但是随着场景复杂度的深入,分层过多带来的弊端逐渐显露出来:在核心任何一个功能增强,都需要对 Platform、Embedder、Shell 三层同时做改造,并且因为要保证 Embedder 和 Shell 层的向兼容性,Embedder 层变得越来越臃肿。
以 Embedder 中负责引擎初始化的 FlutterEngineInitialize 函数为例:
- 其代码行数约有460行;
- 函数入参合法性校验代码有80行;
- 用于配置启动参数的 FlutterProjectArgs 结构体有34的成员;
- 用于配置渲染模式的 FlutterRenderConfig 内嵌全部4中渲染模式配置,每种配置10+个不同的成员;
- 大量的函数指针 Callback.
虽然现在无法判断未来 Flutter Desktop 的实现是否会一直基于 Embedder API 来实现,但基于目前时间点来看,基于 Embedder API 带来的扩展复杂度成本已经逐步掩盖了 Embedder API 所带来封装收益。
后面我们会进一步分析 FlutterEngine 桌面端核心流程,并尝试去对齐一些目前移动端支持、但桌面端暂未支持的能力,服务于钉钉业务的同时,也希望能为 Flutter 生态贡献一份力量。