1 前言

在钉钉 Flutter 桌面端落地过程中,我们遇到了很多仅仅依赖 Flutter 官方文档无法解决的问题,例如:桌面端集成模式问题、内存泄露问题、卡顿问题、光标焦点异常问题等。由于无法直接通过官方文档得到答案,我们便尝试通过分析源码实现和设计文档来寻找解决办法。虽然最终大部分问题得以解决,但是在这过程中有两点一直困扰我们:
  1. Flutter 生态中桌面端相关资料极少。少部分官方公开资料也仅有比较宽泛的介绍,缺少详细方案设计信息;业界对 FlutterEngine 架构分析和讨论,大多也仅仅设计移动端,桌面端相关内容很少涉及;
  2. 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 桌面端与移动端引擎实现差异主要聚焦在两部分:
  1. 是否使用 Embedder API;
  2. 如何处理 Windows Resize。
下面针对上述两点分别介绍一下。

3.1 Embedder API

关于 Flutter 架构设计,最权威的资料当然来自官方团队提供文档(来源[3]):
Flutter 整体设计上三层结构非常清晰,在不同平台实现架构上又会结合平台特点做适当调整和优化:
注:由于 Web 平台下的实现与其它平台有较大差异、且钉钉在实际应用中并未涉及,因此在此不再分析。
无论是移动端还是桌面端,Flutter 三层结构中的 Framework(Dart) 以及 Engine(C++) 实现是共享的,实现上的主要差异聚焦在 Platform 集成上。
在梳理 Flutter Desktop 桌面端发展脉络时我们已经知道,桌面端实现整体建立在 Embedder API 基础之上(Commit[4]):
Embedder API(资料[5])在设计上主要服务于嵌入式场景,即其提供一组平台无关的通用接口,在接口内封装 FlutterEngine 作为「平台无关图形窗口工具套件」的核心实现。使用者仅需按照接口定义传入所需依赖,即可将 Flutter 作为一个动态库接入全新平台。
通过上面的架构图我们可知,Embedder API 在 Flutter 体系内的定位与 Android 和 iOS 是同级的。Flutter Desktop 基于 Embedder API 来实现,在实现上相比移动端自然要多了一层
如果我们以平台接入的视角来分析一下 Flutter 在端侧的架构,那么我们可以得到以下对比图:
由下至上我们对上图做一下简单说明:
  1. Shell 即 FlutterEngine 平台无关的核心实现部分,如 Dart Runtime、Skia等;
  2. Embedder API是 Flutter 封装的平台无关接口层,其主要价值在于封装底层实现、简化接入流程、降低新平台接入成本;
  3. Android/iOS/macOS/Windows Embedder 是平台接入层,各端根据根据平台规则注入 Flutter 运行所依赖的部分,比如 渲染画布、线程管理、插件机制等;
  4. 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 端对应的核心类图:
通过对比上两张类图,我们可以初步得到以下信息:
  1. 桌面端部分实现相比移动端增加一层跨平台的 Embedder API,虽然理论上可简化部分上层实现,但是因为涉及到多渲染模式图层合成等因素的影响,桌面端整体架构复杂度并不低于移动端
  2. Desktop Resize 主要由 FlutterView 来控制,在移动端端中 FlutterView 实现较薄,主要用于承载 Flutter 所需的画布;但是在桌面端其功能更为复杂,已直接参与到 Flutter 绘制流程中,并在其中起到关键作用;
  3. Shell 层做了较好的抽象,绝大部分场景可做到实现与平台无关;

4.2 流程对比

通过4.1小节的内容我们可知,FlutterEngine 在桌面端和移动端的差异主要聚焦在渲染绘制阶段。
根据 Flutter 官方分享资料我们知道,Flutter 渲染大致分为两个阶段:
  1. 第一阶段主要在 UI 线程工作,大部分由 Dart 层的 Flutter Framework 实现;
  2. 第二阶段在 CPU 线程工作,主要由 FlutterEngine 中的 C++ 层模块实现。
针对以上两点,第一阶段无论什么平台基本都是一致的,下面我们就结合实现大致对比一下 iOS 端与 macOS 端的第二阶段流程差异。
阶段iOSmacOS
1DoDrawDoDraw
2DrawToSurfaceDrawToSurface
3AcquireFrameAcquireFrame
4SubmitSubmit
5PresentTry Present(窗口变化中则同步等待)
6FlushDoFlush
注意:上述流程对比仅供示意说明问题,并非严谨流程图,有很多细节表格中并未体现
阶段 5~6 所示即 3.2 小节讨论到的「Desktop Resize」控制模块,结合 4.1 中的类图,渲染链路最终会沿着红线所表示的链路回到 FlutterView 模块,最终由 FlutterView + FlutterResizeSynchronizer + FlutterResizableBackingStoreProvider + FlutterSurfaceManager 相互配合,完成安全可靠的绘制:
移动端应为不涉及窗口大小变化,因为并无此流程。
通过上述对比我们可知,Flutter 桌面端相比移动端实现,渲染流程主要变化在于增加窗口大小变化管理模块,窗口大小变化和 Flutter 页面渲染存在同步影响
  1. 如果在渲染过程中窗口发生变化,则变化动作需要等待渲染流程结束之后才可响应:可能导致 Native 主进程卡顿;
  2. 如果 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 生态贡献一份力量。

参考资料

[1]
flutter-desktop-embedding: https://github.com/google/flutter-desktop-embedding
[2]
Desktop Embedding for Flutter: https://groups.google.com/g/flutter-desktop-embedding-dev
[3]
来源: https://docs.flutter.dev/resources/architectural-overview
[4]
Commit: https://github.com/google/flutter-desktop-embedding/commit/1940026b543a94f1bc791b1f21052d63522013b9
[5]
Embedder API: https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-Embedders
[6]
How to handle resize in desktop embeddings: https://docs.google.com/document/d/1OTy-qCGdP7tYfrEKCNX9A24sgnx5vshfK6FupfniyxA/edit#
[7]
Handling flutter view resizing on macOS: https://docs.google.com/document/d/1slGllp1Jhde7wkF6snqGhdrZwHV1VVmXeIF3f0t24JU/edit#
[8]
Support double buffering for window resizing: https://docs.google.com/document/d/1allwMZXgX9gGVPguFy3-XydjXEIJgYR1Uhz8Vhm9Rrs/edit#
[9]
flutter-desktop-embedding: https://github.com/google/flutter-desktop-embedding
继续阅读
阅读原文