Flutter的两种编译模式

本文最后更新于:2023年8月14日 下午

Flutter 的两种编译模式

使用 Flutter 构建过 App 的人一定有一个困惑,就是 Flutter 编译出的产物到底是什么玩意。有时候分为几个文件,有时候是一个动态库,真的叫人摸不着头脑。

本文详细解释一下 Flutter 的编译模式。


编译模式的分类

编程语言要达到可运行的目的需要经过编译,一般地来说,编译模式分为两类:JIT 和 AOT。

JIT

JIT 全称 Just In Time (即时编译),典型的例子就是 v8,它可以即时编译并运行 JavaScript。所以你只需要输入源代码字符串,v8 就可以帮你编译并运行代码。通常来说,支持 JIT 的语言一般能够支持自省函数(eval),在运行时动态地执行代码。

JIT 模式的优势是显而易见的,可以动态下发和执行代码,而不用管用户的机器是什么架构,为应用的用户提供丰富而动态地内容。

但 JIT 的劣势也是显而易见的,大量字符串的代码很容易让 JIT 编译器花费很多时间和内存进行编译,给用户带来的直接感受就是应用启动慢。

AOT

AOT 全称 Ahead Of Time(事前编译),典型的例子就是 C/C++,LLVM 或 GCC 通过编译并生成 C/C++ 的二进制代码,然后这些二进制通过用户安装并取得执行权限后才可以通过进程加载执行。

AOT 的优势也是显而易见的,事先编译好的二进制代码,加载和执行的速度都会非常快。(所以编程语言速度排行榜上前列都是 AOT 编译类语言)这样的速度可以在密集计算场景下给用户带来非常好的体验,比如大型游戏的引擎渲染和逻辑执行。

但是 AOT 的劣势也是显而易见的,编译需要区分用户机器的架构,生成不同架构的二进制代码。除了架构,二进制代码本身也会让用户下载的安装包比较大。二进制代码一般需要取得执行权限才可以执行,所以无法在权限比较严格的系统中进行动态更新(如 iOS)。


Dart的编译模式

Flutter 使用 Dart 作为编程语言,自然其编译模式也脱离不了 Dart 的干系。首先我们需要了解一下 Dart 所支持的编译模式。

Script:最普通的 JIT 模式,在 PC 命令行调用 Dart VM 执行 Dart 源代码文件即是这种模式;

Script Snapshot:JIT 模式,和上一个不同的是,这里载入的是已经 token 化的 Dart 源代码,提前执行了上一步的

lexer 步骤;

Application Snapshot:JIT 模式,这种模式来源于 Dart VM 直接载入源码后 dump 出数据。Dart VM

通过这种数据启动会更快。不过值得一提的是这种模式是区分架构的,在 x64 上生成的数据不可以给 arm 使用;

AOT:AOT模式,直接将 Dart 源码编译出 .S 文件,然后通过汇编器生成对应架构的代码。

总结一下刚才的列表,可以发现:


Flutter的编译模式

Flutter 完全采用了 Dart,按道理来说编译模式一致才是,但是事实并不是这样。由于 Android 和 iOS平台的生态差异,Flutter 也衍生出了非常丰富的编译模式。

Script:同 Dart Script 模式一致,虽然 Flutter 支持,但暂未看到使用,毕竟影响启动速度;

Script Snapshot:同 Dart Script Snapshot 一致,同样支持但未使用,Flutter

有大量的视图渲染逻辑,纯 JIT 模式影响执行速度;

Kernel Snapshot:Dart 的 bytecode模式,与 Application Snapshot 不同,bytecode

模式是不区分架构的。Kernel Snapshot 在 Flutter 项目内也叫 Core Snapshot。bytecode模式可以归类为 AOT 编译;

Core JIT:Dart 的一种二进制模式,将指令代码和 heap 数据打包成文件,然后在 VM 和 isolate

启动时载入,直接标记内存可执行,可以说这是一种 AOT 模式。Core JIT 也被叫做 AOTBlob;

AOT Assembly: 即 Dart 的 AOT 模式。直接生成汇编源代码文件,由各平台自行汇编。

可以看出来,Flutter 将 Dart 的编译模式复杂化了,多了不少概念,要一下叙述清楚是比较困难的,所以我们着重从 Flutter 应用开发的各个阶段来解读。


开发阶段的编译模式

在开发阶段,我们需要 Flutter 的 Hot Reload 和 Hot Restart 功能,方便 UI 快速成型。同时,框架层也需要比较高的性能来进行视图渲染展现。因此开发模式下,Flutter 使用了 Kernel Snapshot 模式编译。

在打包产物中,你将发现几样东西:

isolate_snapshot_data:用于加速 isolate 启动,业务无关代码,固定,仅和 flutter engine

版本有关;

platform.dill:和 Dart VM 相关的 kernel 代码,仅和 Dart 版本以及 engine

编译版本有关。固定,业务无关代码;

vm_snapshot_data: 用于加速 Dart VM 启动的产物,业务无关代码,仅和 flutter engine 版本有关;

kernel_blob.bin:业务代码产物


生产阶段的编译模式

在生产阶段,应用需要的是非常快的速度,所以 Android 和 iOS target 毫无意外地都选择了 AOT 打包。不过由于平台特性不同,打包模式也是天壤之别。

首先我们很容易认识到 iOS 平台上做法的原因:App Store 审核条例不允许动态下发可执行二进制代码。

所以在 iOS 上,除了 JavaScript,其他语言运行时的实现都选择了 AOT(比如 OpenJDK 在 iOS 实现就是 AOT)。

在 Android 上,Flutter 的做法有点意思:支持了两种不同的路子。

Core JIT 的打包产物有 4 个:isolate_snapshot_data、vm_snapshot_data、isolate_snapshot_instr、vm_snapshot_instr。我们不认识的产物只有 2 个:isolate_snapshot_instr 和 vm_snapshot_instr,其实它俩代表着 VM 和 isolate 启动后所承载的指令等数据。在载入后,直接将该块内存执行即可。

Android 的 AOT Assembly 打包方式很容易让人想到需要支持多架构,无疑增大了代码包,并且该处代码需要从 JNI 调用,远不如 Core JIT 的 Java API 方便。所以 Android 上默认使用 Core JIT 打包,而不是 AOT Assembly。


Flutter Engine 对编译模式的支持

在我的上篇文章:Flutter原理简解 中提到,Engine 承载了 Dart 运行时,毫无疑问 Engine 需要和打包出来的代码对的上号才行。

在 Engine 的编译模式中,Flutter 是这样选择的:

所以我们可以看到,Flutter 的编译模式是完全根据 Engine 的支持度来设计的。


结论

Flutter 是一种高性能的、可跨平台的、动态化应用开发方案。

在 iOS 和 Android 平台上,动态化完全可由 Kernel Snapshot 打包实现,并且产物是一致通用的。不过目前通过打包工具进行了阉割,只能生成 debug 产物。

并且如果不需要动态化,同样可以打包出拥有更高执行性能的二进制库文件使用。这个特性目前就已经支持,有了理论的支持,我们就可以着手做改造的事了。

原文链接:

https://blog.csdn.net/u010960265/article/details/81361711


Flutter的两种编译模式
http://bestkele.com/2019/10/23/flutter/flutter-compiler/
作者
kele
发布于
2019年10月23日
许可协议