深入理解Flutter的编译原理与优化
导读:对于开发者而言,Flutter 工程和我们的 Android/iOS 工程有何差别?Flutter 的渲染和事件传递机制如何工作?构建缓慢或出错又如何去定位,修改和生效呢?凡此种种,都需要对 Flutter 从设计,开发构建,到最终运行有一个全局视角的观察。本文由闲鱼技术团队出品,将以一个简单的 hello_flutter 为例,介绍下 Flutter 相关原理及定制与优化。
Flutter 简介
Flutter 的架构主要分成三层:Framework,Engine 和 Embedder。
Framework 使用 dart 实现,包括 Material Design 风格的 Widget,Cupertino (针对 iOS)风格的 Widgets,文本/图片/按钮等基础 Widgets、渲染、动画、手势等。此部分的核心代码是:flutter 仓库下的 flutter package,以及 sky_engine 仓库下的 io,async,ui (dart:ui 库提供了 Flutter 框架和引擎之间的接口)等 package。
Engine 使用 C++ 实现,主要包括:Skia,Dart 和 Text。Skia 是开源的二维图形库,提供了适用于多种软硬件平台的通用 API。其已作为 Google Chrome,Chrome OS,Android, Mozilla Firefox, Firefox OS 等其他众多产品的图形引擎,支持平台还包括 Windows 7+,macOS 10.10.5+,iOS8+,Android4.1+,Ubuntu14.04+ 等。
Dart 部分主要包括:Dart Runtime,Garbage Collection (GC),如果是 Debug 模式的话,还包括 JIT (Just In Time)支持。Release 和 Profile 模式下,是 AOT (Ahead Of Time)编译成了原生的 arm 代码,并不存在 JIT 部分。Text 即文本渲染,其渲染层次如下:衍生自 minikin 的 libtxt 库(用于字体选择,分隔行)。HartBuzz 用于字形选择和成型。Skia 作为渲染/GPU 后端,在 Android 和 Fuchsia 上使用 FreeType 渲染,在 iOS 上使用 CoreGraphics 来渲染字体。
Embedder 是一个嵌入层,即把 Flutter 嵌入到各个平台上去,这里做的主要工作包括渲染 Surface 设置,线程设置,以及插件等。从这里可以看出,Flutter 的平台相关层很低,平台(如 iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在 Flutter 内部,这就使得它具有了很好的跨端一致性。
Flutter 工程结构
本文使用开发环境为 flutter beta v0.3.1,对应的 engine commit:09d05a389。
以 hello_flutter 工程为例,Flutter 工程结构如下所示:
其中 ios 为 iOS 部分代码,使用 CocoaPods 管理依赖,android 为 Android 部分代码,使用 Gradle 管理依赖,lib 为 dart 代码,使用 pub 管理依赖。类似 iOS 中 Cocoapods 对应的 Podfile 和 Podfile.lock,pub 下则是 pubspec.yaml 和 pubspec.lock。
Flutter 模式
对于 Flutter,它支持常见的 debug,release,profile 等模式,但它又有其不一样。
Debug 模式:对应了 Dart 的 JIT 模式,又称检查模式或者慢速模式。支持设备,模拟器(iOS/Android),此模式下打开了断言,包括所有的调试信息,服务扩展和 Observatory 等调试辅助。此模式为快速开发和运行做了优化,但并未对执行速度,包大小和部署做优化。Debug 模式下,编译使用 JIT 技术,支持广受欢迎的亚秒级有状态的 hot reload。
Release 模式:对应了 Dart 的 AOT 模式,此模式目标即为部署到终端用户。只支持真机,不包括模拟器。关闭了所有断言,尽可能多地去掉了调试信息,关闭了所有调试工具。为快速启动,快速执行,包大小做了优化。禁止了所有调试辅助手段,服务扩展。
Profile 模式:类似 Release 模式,只是多了对于 Profile 模式的服务扩展的支持,支持跟踪,以及最小化使用跟踪信息需要的依赖,例如,observatory 可以连接上进程。Profile 并不支持模拟器的原因在于,模拟器上的诊断并不代表真实的性能。
鉴于 Profile 同 Release 在编译原理等上无差异,本文只讨论 Debug 和 Release 模式。
事实上 flutter 下的 iOS/Android 工程本质上依然是一个标准的 iOS/Android 的工程,flutter 只是通过在 BuildPhase 中添加 shell 来生成和嵌入 App.framework 和 Flutter.framework (iOS),通过 gradle 来添加 flutter.jar 和 vm/isolate_snapshot_data/instr (Android)来将 Flutter 相关代码编译和嵌入原生 App 而已。因此本文主要讨论因 flutter 引入的构建,运行等原理。编译 target 虽然包括 arm,x64,x86,arm64,但因原理类似,本文只讨论 arm 相关(如无特殊说明,android 默认为 armv7)。
Flutter 代码的编译与运行(iOS)
Release 模式下的编译
release 模式下,flutter 下 iOS 工程中 dart 代码构建链路如下所示:
其中 gen_snapshot 是 dart 编译器,采用了 tree shaking (类似依赖树逻辑,可生成最小包,也因而在 Flutter 中禁止了 dart 支持的反射特性)等技术,用于生成汇编形式的机器代码,再通过 xcrun 等编译工具链生成最终的 App.framework。换句话说,所有的 dart 代码,包括业务代码,三方 package 代码,它们所依赖的 flutter 框架代码,最终将会变成 App.framework。
tree shaking 功能位于 gen_snapshot 中,对应逻辑参见:
engine/src/third_party/dart/runtime/vm/compiler/aot/precompiler.cc
dart 代码最终对应到 App.framework 中的符号如下所示:
事实上,类似 Android Release 下的产物(见下文),App.framework 也包含了 kDartVmSnapshotData,kDartVmSnapshotInstructions,kDartIsolateSnapshotData,kDartIsolateSnapshotInstructions 四个部分。为什么 iOS 使用 App.framework 这种方式,而不是 Android 的四个文件的方式呢?原因在于在 iOS 下,因为系统的限制,Flutter 引擎不能够在运行时将某内存页标记为可执行,而 Android 是可以的。
Flutter.framework 对应了 Flutter 架构中的 engine 部分,以及 Embedder。实际中 Flutter.framework 位于 flutter 仓库的/bin/cache/artifacts/engine/ios*下,默认从 google 仓库拉取。当需要自定义修改的时候,可通过下载 engine 源码,利用 Ninja 构建系统来生成。
Flutter 相关代码的最终产物是:App.framework (dart 代码生成)和 Flutter.framework (引擎)。从 Xcode 工程的视角看,Generated.xcconfig 描述了 Flutter 相关环境的配置信息,然后 Runner 工程设置中的 Build Phases 新增的 xcode_backend.sh 实现了 Flutter.framework 的拷贝(从 Flutter 仓库的引擎到 Runner 工程根目录下的 Flutter 目录)与嵌入和 App.framework 的编译与嵌入。最终生成的 Runner.app 中 Flutter 相关内容如下所示:
其中 flutter_assets 是相关的资源,代码则是位于 Frameworks 下的 App.framework 和 Flutter.framework。
Release 模式下的运行
Flutter 相关的渲染,事件,通信处理逻辑如下所示:
其中 dart 中的 main 函数调用栈如下:
Debug 模式下的编译
Debug 模式下 flutter 的编译,结构类似 Release 模式,差异主要表现为两点:
1. Flutter.framework
因为是 Debug,此模式下 Framework 中是有 JIT 支持的,而在 Release 模式下并没有 JIT 部分。
2. App.framework
不同于 AOT 模式下的 App.framework 是 Dart 代码对应的本地机器代码,JIT 模式下,App.framework 只有几个简单的 API,其 Dart 代码存在于 snapshot_blob.bin 文件里。这部分的 snapshot 是脚本快照,里面是简单的标记化的源代码。所有的注释,空白字符都被移除,常量也被规范化,也没有机器码,tree shaking 或者是混淆。
App.framework 中的符号表如下所示:
对 Runner.app/flutter_assets/snapshot_blob.bin 执行 strings 命令可以看到如下内容:
Debug 模式下 main 入口的调用堆栈如下:
Flutter 代码的编译与运行(Android)
鉴于 Android 和 iOS 除了部分平台相关的特性外,其他逻辑如 Release 对应 AOT,Debug 对应 JIT 等均类似,此处只涉及两者不同。
Release 模式下的编译
release 模式下,flutter 下 Android 工程中 dart 代码整个构建链路如下所示:
其中 vm/isolate_snapshot_data/instr 内容均为 arm 指令,将会在运行时被 engine 载入,并标记 vm/isolate_snapshot_instr 为可执行。vm_中涉及 runtime 等服务(如 gc),用于初始化 DartVM,调用入口见 Dart_Initialize (dart_api.h)。isolate__则是对应了我们的 App 代码,用于创建一个新 isolate,调用入口见 Dart_CreateIsolate (dart_api.h)。
flutter.jar 类似 iOS 的 Flutter.framework,包括了 engine 部分的代码(Flutter.jar 中的 libflutter.so),以及一套将 Flutter 嵌入 Android 的类和接口(FlutterMain,FlutterView,FlutterNativeView 等)。实际中 flutter.jar 位于 flutter 仓库的/bin/cache/artifacts/engine/android*下,默认从 google 仓库拉取。当需要自定义修改的时候,可通过下载 engine 源码,利用 Ninja 构建系统来生成 flutter.jar。
以 isolate_snapshot_data/instr 为例,执行 disarm 命令结果如下:
其 Apk 结构如下所示:
APK 新安装之后,会根据一个 ts 的判断(packageinfo 中的 versionCode 结合 lastUpdateTime)来决定是否拷贝 APK 中的 assets,拷贝后内容如下所示:
isolate/vm_snapshot_data/instr 均最后位于 app 的本地 data 目录下,而这部分又属于可写内容,因此可以通过下载并替换的方式,完成 App 的整个替换和更新。
Release 模式下的运行
Debug 模式下的编译
类似 iOS 的 Debug/Release 的差别,Android 的 Debug 与 Release 的差异主要包括以下两部分:
1. flutter.jar
区别同 iOS
2. App 代码部分
位于 flutter_assets 下的 snapshot_blob.bin,同 iOS。
在介绍了 iOS/Android 下的 Flutter 编译原理后,下面着重描述下如何定制 flutter/engine 以完成定制和优化。鉴于 Flutter 处于敏捷的迭代中,现在的问题后续不一定是问题,因而此部分并不是要去解决多少问题,而是选取不同类别的问题来说明解决思路。
Flutter 构建相关的定制与优化
Flutter 是一个很复杂的系统,除了上述提到的三层架构中的内容外,还包括 Flutter Android Studio (Intellij)插件,pub 仓库管理等。但我们的定制和优化往往是在 flutter 的工具链相关,具体代码位于 flutter 仓库的 flutter_tools 包。接下来举例说明下如何对这部分做定制。
Android 部分
相关内容包括 flutter.jar,libflutter.so (位于 flutter.jar 下),gen_snapshot,flutter.gradle,flutter (flutter_tools)。
1. 限定 Android 中 target 为 armeabi
此部分属于构建相关,逻辑位于 flutter.gradle 下。当 App 是通过 armeabi 支持 armv7/arm64 的时候,需要修改 flutter 的默认逻辑。如下所示:
因为 gradle 本身的特点,此部分修改后直接构建即可生效。
2. 设定 Android 启动时默认使用第一个 launchable-activity,此部分属于 flutter_tools 相关,修改如下:
这里的重点不是如何去修改,而是如何去让修改生效。原理上来说,flutter run/build/analyze/test/upgrade 等命令实际上执行的都是 flutter (flutter_repo_dir/bin/flutter)这一脚本,再通过脚本通过 dart 执行 flutter_tools.snapshot (通过 packages/flutter_tools 生成)。其逻辑如下:
不难看出要重新构建 flutter_tools,可以删除 flutter_repo_dir/bin/cache/flutter_tools.stamp (这样重新生成一次),或者屏蔽掉 if/fi 判断(每一次都会重新生成)。
3. 如何在 Android 工程 Debug 模式下使用 release 模式的 flutter
当开发者在研发中发现 flutter 有些卡顿时,猜测可能是逻辑的原因,也可能是因为是 Debug 下的 flutter。此时可以构建 release 下的 apk,也可以将 flutter 强制修改为 release 模式如下:
iOS 部分
相关内容包括:Flutter.framework,gen_snapshot,xcode_backend.sh,flutter (flutter_tools)。
1. 优化构建过程中反复替换 Flutter.framework 导致的重新编译
此部分逻辑属于构建相关,位于 xcode_backend.sh 中,Flutter 为了保证每次获取到正确的 Flutter.framework,每次都会基于配置(见 Generated.xcconfig 配置)查找和替换 Flutter.framework,但这也导致了工程中对此 Framework 有依赖部分代码的重新编译,修改如下:
2. 如何在 iOS 工程 Debug 模式下使用 release 模式的 flutter
只需要将 Generated.xcconfig 中的 FLUTTER_BUILD_MODE 修改为 release,FLUTTER_FRAMEWORK_DIR 修改为 release 对应的路径即可。
3. armv7 的支持
原始文章请参见:
https://github.com/flutter/engine/wiki/iOS-Builds-Supporting-ARMv7
事实上 flutter 本身是支持 iOS 下的 armv7 的,但目前并未提供官方支持,需要自行修改相关逻辑,具体如下:
a.默认的逻辑可以生成
Flutter.framework (arm64)
b.修改 flutter 以使得 flutter_tools 可以每次重新构建,修改 build_aot.dart 和 mac.dart,将相关针对 iOS 的 arm64 修改为 armv7,修改 gen_snapshot 为 i386 架构。
其中 i386 架构下的 gen_snapshot 可通过以下命令生成:
这里有一个隐含逻辑:
构建 gen_snapshot 的 CPU 相关预定义宏(x86_64/__i386 等),目标 gen_snapshot 的 arch,最终的 App.framework 的架构整体上要保持一致。即 x86_64->x86_64->arm64 或者 i386->i386->armv7。
c.在 iPhone4S 上,会发生因 gen_snapshot 生成不被支持的 SDIV 指令而造成 EXC_BAD_INSTRUCTION (EXC_ARM_UNDEFINED)错误,可通过给 gen_snapshot 添加参数–no-use-integer-division 实现(位于 build_aot.dart)。其背后的逻辑如下图所示:
d.基于a和b生成的 Flutter.framework,将其 lipo create 生成同时支持 armv7 和 arm64 的 Flutter.framework。
e.修改 Flutter.framework 下的 Info.plist,移除
同理,对于 App.framework 也要作此操作,以免上架后会受到 App Thining 的影响。
flutter_tools 的调试
例如我们想了解 flutter 在构建 debug 模式下的 apk 的时候,具体执行的逻辑如何,可以按照下面的思路走:
a.了解 flutter_tools 的命令行参数
b.以 dart 工程形式打开 packages/flutter_tools,基于获得的参数修改 flutter_tools.dart,设置命令行 dart app 即可开始调试。
定制 engine 与调试
假设我们在 flutter beta v0.3.1 的基础上进行定制与业务开发,为了保证稳定,一定周期内并不升级 SDK,而此时,flutter 在 master 上修改了某个 v0.3.1 上就有的 bug,记为 fix_bug_commit。如何才能跟踪和管理这种情形呢?
1. flutter beta v0.3.1 指定了其对应的 engine commit 为:09d05a389,见
flutter/bin/internal/engine.version。
2. 获取 engine 代码
3. 因为 2 中拿到的是 master 代码,而我们需要的是特定 commit (09d05a389) 对应的代码库,因而从此 commit 拉出新分支:custom_beta_v0.3.1。
4. 基于 custom_beta_v0.3.1(commit:09d05a389),执行 gclient sync,即可拿到对应 flutter beta v0.3.1 的所有 engine 代码。
5. 使用 git cherry-pick fix_bug_commit 将 master 的修改同步到 custom_beta_v0.3.1,如果修改有很多对最新修改的依赖,可能会导致编译失败。
6. 对于 iOS 相关的修改执行以下代码:
如果需要调试 Flutter.framework 源代码,构建的时候命令如下:
用生成产物替换掉 flutter 中的 Flutter.framework 和 gen_snapshot,即可调试 engine 源代码。
7. 对于 Android 相关的修改执行以下代码:
即可生成针对 Android 的 arm&debug/release/profile
的产物。可用构建产物替换
flutter/bin/cache/artifacts/engine/android*下的 gen_snapshot 和 flutter.jar。
如果对文本的内容有疑问或指正,欢迎告知我们。
另闲鱼技术团队诚聘各路英才,flutter,C++,iOS/Android,Java 都要,欢迎简历来砸。联系邮箱: kylewong.wk@alibaba-inc.com。
参考文档:
1. Flutter’s modes
https://github.com/flutter/flutter/wiki/Flutter%27s-modes
2. iOS Builds Supporting ARMv7
https://github.com/flutter/engine/wiki/iOS-Builds-Supporting-ARMv7
3. Contributing to the Flutter engine
https://github.com/flutter/engine/blob/master/CONTRIBUTING.md
4. Flutter System Architecture
- https://docs.google.com/presentation/d/1cw7A4HbvM_Abv320rVgPVGiUP2msVs7tfGbkgdrTy0I/edit
5. The magic of flutter
- https://docs.google.com/presentation/d/1B3p0kP6NV_XMOimRV09Ms75ymIjU5gr6GGIX74Om_DE/edit
6. Symbolicating production crash stacks
https://github.com/flutter/engine/wiki/Symbolicating-production-crash-stacks
7. flutter.io
https://flutter.io/docs
8. 获取本文使用的源代码
https://github.com/FlutterRepo/hello_flutter.git
你也许感兴趣的:
- 【外评】Flutter 是否面临死亡?
- 【外评】Flutter 团队有多大?
- Dart 3.1 和 Flutter 3.13 发布,你准备好抛弃 HTML 了吗?
- 准备好抛弃 HTML 了吗?Dart 3.1 和 Flutter 3.13 发布
- 深入理解 Flutter 图片加载原理 | 京东云技术团队
- Flutter 即将占领整个 Web 开发
- React Native 与 Flutter 的跨平台王位之争的360 度全方位观测
- 为什么 Flutter 还不是最成熟的跨端框架?
- Flutter 并不像你想象中的那么完美
- React Native 官方团队怎么看待 Flutter 的?
你对本文的反应是: