Flutter技术剖析

举报
Tracy 发表于 2019/10/15 14:17:34 2019/10/15
【摘要】 Flutter已经开源了三年,但是最近两年才开始在开源社区活跃起来,尤其是最近还发布了Preview 1版本。作为可以实现一套代码同时在iOS、Android平台上运行的又一个新的UI框架,Flutter提供给开发者的不仅仅是高速实现,还有高质量、流畅的UI。在加上其开源免费,因而受到很多移动开发者和企业的喜爱。跨平台开发经常需要面对的一个问题是,Android 或 iOS 的平台不同,那么...

Flutter已经开源了三年,但是最近两年才开始在开源社区活跃起来,尤其是最近还发布了Preview 1版本。作为可以实现一套代码同时在iOS、Android平台上运行的又一个新的UI框架,Flutter提供给开发者的不仅仅是高速实现,还有高质量、流畅的UI。在加上其开源免费,因而受到很多移动开发者和企业的喜爱。

跨平台开发经常需要面对的一个问题是,Android 或 iOS 的平台不同,那么使用同一套代码,它们是怎么进行渲染的呢?本文以“hello_flutter”为例,首先介绍 Flutter 的设计原则,然后讨论定制和优化,并为对 Flutter 感兴趣的开发人员提供了一些可遵循的步骤。

Flutter基础

架构

Flutter框架从上到下包含三个不同的层:框架、引擎和嵌入器。Flutter 的框架使用 Dart 实现,提供了 Material 风格的小部件、Cupertino 风格的小部件(用于 iOS)、文本 / 图像 / 按钮小部件、渲染、动画、手势等。该层的核心代码包含了 flutter 代码库的包和 sky_engine 代码库的包(dart:ui 库提供了 flutter 框架和引擎之间的接口),例如 io、async 和 ui 包。

Flutter 的引擎是用 C++ 实现的,并包含了 Skia、Dart 和 Text。Skia 是一个开源的 2D 图形库,为各种硬件和软件平台提供通用 API。它是谷歌 Chrome、Chrome OS、Android、Mozilla Firefox、Firefox OS 等产品的图形引擎。支持的平台包括 Windows7+、macOS 10.10.5+、iOS8+、Android4.1+、Ubuntu14.04+ 等。

引擎的 Dart 部分主要包括 Dart 运行时和垃圾回收(GC)。如果 Flutter 在调试模式下运行,则还包括 JIT(Just in Time)支持,而如果是在发布模式下,则通过 AOT(Ahead of Time)将 Dart 代码编译为原生的“arm”代码,这个时候就没有 JIT。Text 是指以下的文本渲染库:libtxt 库(用于字体选择和分隔线),派生自 minikin 和 HartBuzz(用于字形和图形)。Skia 充当渲染后端,在 Android 上使用 FreeType 渲染,在 iOS 上使用 Fuchsia 和 CoreGraphics 渲染。

嵌入器可将 Flutter 嵌入到各种平台中。它的主要任务是渲染 Surface 设置、线程设置和插件。我们可以看到,Flutter 的平台相关层是最小的,其中平台(例如 iOS)只提供画布,其余与渲染相关的逻辑发生在 Flutter 内部,从而实现良好的跨平台一致性。

工程结构

一个完整的Flutter工程的项目结构如下图所示:其中,“ios”是 iOS 的代码,使用 CocoaPods 来管理依赖项,“android”是 Android 的代码,使用 Gradle 来管理依赖项,“lib”是 Dart 代码,使用 pub 来管理依赖项。pub 中与 Cocoapods 的 Podfile 和 Podfile.lock 相对应的分别是 pubspec.yaml 和 pubspec.lock。

模式

Flutter 支持常见的模式,包括调试、发布和分析,但它们之间存在一些区别。

Flutter 的调试模式对应于 Dart 的 JIT 模式,也称为检查模式或慢模式,并支持 iOS 和 Android 的设备和模拟器。在这个模式下,可以启用断言功能,包括所有调试信息、服务扩展和调试辅助工具,如“observatory”。这个模式针对快速开发进行了优化,但并没有针对运行速度、程序包大小或部署进行过优化。在这种模式下,采用了基于 JIT 的编译技术,支持流行的亚秒级有状态热重载。

Flutter 的发布模式对应于 Dart 的 AOT 模式,该模式的目标是部署到最终用户的设备上,支持真实设备而不是模拟器。在此模式下,所有断言都被禁用,为了尽可能多地删除调试信息,还会禁用所有调试工具。这个模式针对快速启动、快速运行和包大小进行了优化,同时禁止所有调试辅助和服务扩展。

Flutter 的分析模式与发布模式类似,只是添加了对服务扩展和跟踪的支持,并最小化使用跟踪信息所需的依赖性。例如,“observatory”可以连到进程上。分析模式不支持模拟器,因为模拟器上的诊断不能代表实际性能。

由于 Flutter 的分析模式和发布模式在编译方面没有差异,因此本文仅讨论调试模式和发布模式。

实际上,使用 Flutter 开发的 iOS 或 Android 项目仍然是标准的 iOS 或 Android 项目。Flutter 通过在 BuildPhase 中添加 shell 来生成并嵌入 App.framework 和 Flutter.framework(iOS),并通过 Gradle 添加 flutter.jar 和 vm/isolate_snapshot_data/instr(Android)来编译相关代码并将其嵌入到原生应用程序中。因此,本文主要讨论 Flutter 引入的构建和运行原则。尽管编译目标包括 arm、x64、x86 和 arm64,但它们的原理都很类似,所以本文仅讨论与 arm 相关的原则。(如果没有特殊描述,Android 默认为 armv7。)

iOS 的代码编译和执行

下面就以 iOS 的代码编译和执行为例,讲解iOS项目在发布模式下的编译和执行过程。

在发布模式下编译

在发布模式下,iOS 项目的 Dart 代码构建过程如下图所示:在图中,gen_snapshot 是 Dart 编译器,它使用摇树优化技术(类似于可生成最小包的依赖树逻辑,因此在 Flutter 中禁用 Dart 支持的反射)生成汇编形式的机器码,然后通过编译工具链(如 xcrun)生成最终 App.framework。换句话说,对于所有的 Dart 代码,包括业务逻辑代码和第三方软件包代码,它们所依赖的 Flutter 框架(Dart)代码最终会变成 App.framework。可以通过“engine/src/third_party/dart/runtime/vm/compiler/aot/precompiler.cc”来查看相关的逻辑。

事实上,与 Android 类似,App.framework 也包括了四个部分:kDartVmSnapshotData、kDartVmSnapshotInstructions、kDartIsolateSnapshotData 和 kDartIsolateSnapshotInstructions。为什么 iOS 使用 App.framework 而不是像 Android 那样的四个文件?由于 iOS 系统的限制,Flutter 引擎无法在运行时将内存页标记为可执行,而在 Android 下则可以。

Flutter.framework 对应于 Flutter 架构中的引擎和嵌入器。实际上,Flutter.framework 位于 flutter 代码库的 /bin/cache/artifacts/engine/ios * 中,默认是从谷歌的代码库中提取的。当需要自定义变更时,可以使用 Ninja 系统下载和构建与引擎相关的代码。

Flutter 相关代码的最终产物是 App.framework(由 Dart 代码生成)和 Flutter.framework(引擎)。在 Xcode 项目中,Generated.xcconfig 描述了与 Flutter 相关的环境配置信息,在 Runner 项目设置中新增的 xcode_backend.sh 实现了一个副本(从 flutter 框架代码库到 Runner 项目根目录下的 Flutter 目录)和 Flutter.framework 的嵌入以及 App.framework 的编译和嵌入。

所以,在iOS上最终生成的 Runner.app 中与 Flutter 相关的文件如下:flutter_assets 是 Flutter 资源,App.framework 和 Flutter.framework 是代码,位于 Frameworks 目录中。

发布模式下运行

与 Flutter 相关的渲染、事件和通信处理的逻辑如下图表示:

在调试模式下,Flutter 的编译结构与发布模式中的编译结构类似。差异主要表现在两点:Flutter.framework和App.framework。 Flutter.framework 在调试模式下,框架包含 JIT 支持,而在发布模式下没有 JIT 支持。 App.framework 与 Flutter.framework 不同,App.framework 是原生机器码,与 AOT 模式中的 Dart 代码对应,而在 JIT 模式下,App.framework 只有几个简单的 API,Dart 代码存在于 snapshot_blob.bin 文件中。这部分代码的快照是带有简单标记的源代码的脚本快照。所有的注释和空格字符都被移除,常量被规格化,不存在机器代码、摇树优化或代码混淆。

Android 的代码编译和执行

除了一些与平台相关的功能之外,Android 其他逻辑(例如对应于 AOT 的发布模式和对应于 JIT 的调试模式)与 iOS 非常相似,只需注意两个关键差异。

####发布模式下编译 在发布模式下,Android Flutter 项目中的 Dart 代码结构如下:其中,vm/isolate_snapshot_data/instr 是 arm 指令,引擎会在运行时加载它们并将其标记为可执行。vm_snapshot_data/instr 用于初始化 DartVM,调用入口为 Dart_Initialize(Dart_api.h)。isolate_snapshot_data/instr 对应于创建新隔离的 App 代码,调用入口为 Dart_CreateIsolate(Dart_api.h)。

Flutter.jar 类似于 iOS 的 Flutter.framework,包括引擎代码(Flutter.jar 中的 libflutter.so)以及一组将 Flutter 嵌入到 Android 中的类和接口(FlutterMain、FlutterView、FlutterNativeView 等)。事实上,flutter.jar 位于 Flutter 代码库的 /bin/cache/artifacts/engine/android* 中,默认从谷歌代码库中拉取。当需要自定义更改时,可以使用 Ninja 系统下载引擎源代码来生成 flutter.jar。

其中,其 APK 结构如下:在全新安装 APK 之后,使用时间戳(结合 versionCode 与 packageinfo 的 lastUpdateTime)来决定是否将 Flutter 相关文件复制到本地 app 数据目录。复制的内容如下:

isolate/vm_snapshot_data/instr 最终位于 app 本地数据目录中,该目录是可写的。因此,可以通过下载和替换这些快照就可以完成 app 的替换和更新。

发布模式下执行

下图显示了发布模式下的执行流程:

在调试模式下编译

与 iOS 的情况一样,Android 中的调试模式和发布模式之间的区别主要在于以下两个组件:flutter.jar和app源码。 flutter.jar 这里的区别与之前针对 iOS 所描述的完全相同。 app 代码 app 代码位于 flutter_assets 下的 snapshot_blob.bin 中,就像 iOS 一样。

在介绍完 Flutter 有关 iOS 和 Android 的编译原理后,我们将重点介绍如何配置 Flutter 及其引擎,以进行自定义和优化。因为 Flutter 使用了敏捷开发模式,所以当前出现的问题在未来可能就不是问题。因此,以下部分不着重于如何解决问题,而是着重于不同类型的场景,这些场景体现了 Flutter 自定义和优化方面的原则。

Flutter 是一个复杂的系统。除了上面提到的三层架构,它还包括 Flutter Android Studio(Intellij)插件、pub 代码库管理和其他各种组件。不过,定制和优化通常与 Flutter 的工具链有关,代码位于 Flutter 代码库的 flutter_tools 包中。我们现在将分别介绍如何针对 Android 和 iOS 进行自定义。

自定义Android

自定义 Android 涉及 flutter.jar、libflutter.so(在 flutter.jar 中)、gen_snapshot、flutter.gradle 和 flutter_tools。在自定义 Flutter 时,需要注意以下事项: 1. 将 Android 中的目标设置为 armeabi。 这是构建过程的一部分,逻辑是在 flutter.gradle 中定义的。如果要应用程序通过 armeabi 支持 armv7/arm64,必须修改 Flutter 的默认逻辑。

由于 Gradle 本身的特性,这部分代码需要在修改后才能生效。 2. 将 Android 设置为在启动时默认使用第一个可启动的 Activity。 这部分与 flutter_tools 有关,修改如下:

原则上,使用“flutter run/build/analyze/test/upgrade”这样的命令实际上运行的是 Flutter 脚本(flutter_repo_dir/bin/flutter),然后再通过这个脚本运行 flutter_tools.snapshot(由 packages/flutter_tools 生成)。逻辑如下:

image.png

很明显,如果要重建 flutter_tools,可以删除 flutter_repo_dir/bin/cache/flutter_tools.stamp(以便重新生成它),或者注释掉 if/fi(每次都重新生成)。 3. 在调试模式下发布 Flutter. 如果你发现 Flutter 在开发中出现延迟,并且猜测这可能是由逻辑或调试模式引起的,那么可以在发布模式下构建 APK,或者将 Flutter 强制改为为发布模式, 修改内容如下:

自定义 iOS

与自定义 iOS 相关的内容包括 Flutter.framework、gen_snapshot、xcode_backend.sh 和 flutter_tools。在自定义 Flutter 时,需要注意以下事项:

1. 在优化期间重复替换 Flutter.framework 导致的重新编译

这部分的逻辑与构建有关,位于 xcode_backend.sh 中。为了确保每次都能获得正确的 Flutter.framework,Flutter 每次都会根据配置查找并替换 Flutter.framework(请参阅 Generated.xcconfig 配置)。不过,这会导致重新编译依赖于这个框架的项目代码。必要的修改如下:2. 在调试模式下发布 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:

./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=armninja -C out/ios_debug_arm

这里有一种隐含的逻辑:构造 gen_snapshot 的预定义宏(x86_64/__ i386 等)。目标 gen_snapshot 的结构和最终的 App.framework 结构必须保持一致。也就是说,使用 x86_64->x86_64->arm64 或 i386->i386->armv7。

c. 在 iPhone4S 上,当 gen_snapshot 生成不受支持的 SDIV 命令时会发生 EXC_BAD_INSTRUCTION(EXC_ARM_UNDEFINED)错误,这可以通过向 gen_snapshot(位于 build_aot.Dart 中)添加参数“——no-use-integer-division”来解决。

d.“lipo -create”在 a 步骤和 b 步骤中生成的 Flutter.framework,以便生成支持 armv7 和 arm64 的 Flutter.framework。

e. 修改 Flutter.framework 中的 Info.plist,并删除如下:

<key>UIRequiredDeviceCapabilities</key><array><string>arm64</string></array>

同样,你必须在 App.framework 上执行相同的操作,以避免受到 AppStore 中应用程序细化的影响。

调试 Flutter 工具

在调试模式下构建 APK 时,如果你想知道 Flutter 的特定执行逻辑,可以采用以下方法:

a. 了解 flutter_tools 命令的参数。b. 将 packages/flutter_tools 作为 Dart 项目打开,并添加新的“Dart Command Line App”配置 将 Dart 文件设置为“flutter_tools.Dart”,将工作目录设置为 Flutter 项目的路径,并将 Program 参数设置为先前获得的参数。

自定义和调试引擎

请考虑以下情形。假设我们基于 Flutter beta v0.3.1 定制和开发服务,为了确保稳定性,SDK 在某段时间内不会升级。同时,Flutter v0.3.1 的 master 分支上修改了一个 bug,标记为 fix_bug_commit。你将如何应对这种情况?

1.Flutter beta v0.3.1 将其相应的引擎代码提交指定为 09d05a389。 关于这部分知识可以参阅:flutter/bin/internal/engine.version。

  1. 获取引擎代码。

  2. 由于 master 代码是在第二步中获得的,我们需要的是与特定提交相对应的代码(09d05a389),所以需要从这次提交拉取一个新分支:custom_beta_v0.3.1。

  3. 在 custom_beta_v0.3.1(commit:09d05a389)上运行“gclient sync”,获取与 flutter beta v0.3.1 相对应的所有引擎代码。

  4. 使用“git cherry-pick fix_bug_commit”将 master 的变更同步到 custom_beta_v0.3.1。如果变更依赖了最新依赖项,则可能会发生编译错误。

  5. 运行以下命令应用与 iOS 相关的变更:

./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=armninja -C out/ios_debug_arm./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=armninja -C out/ios_release_arm./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=armninja -C out/ios_profile_arm./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm64ninja -C out/ios_debug./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm64ninja -C out/ios_release./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm64ninja -C out/ios_profile

要调试 Flutter.framework 源代码,请使用以下命令:

./flutter/tools/gn --runtime-mode=debug --unoptimized --ios --ios-cpu=arm64ninja -C out/ios_debug_unopt

用生成的文件替换 Flutter 中的 Flutter.framework 和 gen_snapshot,这样就可以调试引擎源代码。 7. 最后,运行以下命令应用与 Android 相关的变更

./flutter/tools/gn --runtime-mode=debug --android --android-cpu=armninja -C out/android_debug./flutter/tools/gn --runtime-mode=release --android --android-cpu=armninja -C out/android_release./flutter/tools/gn --runtime-mode=profile --android --android-cpu=armninja -C out/android_profile

然后,使用生成的文件替换 flutter/bin/cache/artifacts/engine/android* 下的 gen_snapshot 和 flutter.jar,以便生成 Android 的 arm 和 debug/release/profile 文件包。


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。