Flutter 引擎 AOT 模式嵌入实战:为第三方 Embedder 构建、编译与打包 AOT 快照
本文面向使用 稳定 C 嵌入 API 的第三方 Embedder 开发者,完整讲解如何让 Flutter 应用脱离 JIT、以 AOT(Ahead-Of-Time)模式运行:从用 GN 构建 AOT 引擎、编译目标架构专用的 gen_snapshot,到生成 kernel 快照与四个 AOT 二进制块、按内存权限约束打包这些块,并最终通过 FlutterProjectArgs 把它们交给 FlutterEngineRun。读完本文,你可以为自定义平台(非 Android/iOS 标准工具链覆盖的平台)完整产出一套可在目标设备上直接加载的 AOT 工件。
一、背景:第三方 Embedder 与 AOT 模式
Flutter 引擎默认以 JIT 模式打包——这种引擎里携带的 Dart VM 无法加载 AOT 快照。如果第三方 Embedder(即用 embedder.h 暴露的稳定 C API 把 Flutter 集成到自己平台上的开发者)想以 AOT 模式发布应用,必须自己完成三件事:
- 构建一个针对 AOT 模式配置的 Flutter 引擎;
- 为目标架构生成 AOT 指令(kernel 快照 → 四个 AOT 二进制块);
- 在运行时把二进制块按正确的内存权限映射进地址空间,并配置引擎以 AOT 方式启动。
这四个二进制块的职责在 Flutter-engine-operation-in-AOT-Mode.md 中有权威定义:VM 快照数据(isolate 间共享的 Dart 堆初始状态,应位于只读数据段)、VM 指令(所有 isolate 共享的 AOT 例程与 stub,必须位于可执行文本段)、Isolate 快照数据(含 isolate 特定信息的堆初始状态,只读)、Isolate 指令(该 isolate 实际执行的 AOT 代码,可执行)。数据快照只需 READ 权限,指令快照必须 READ + EXECUTE——这一约束直接决定了后文的打包方式。
二、构建 AOT 模式的 Flutter 引擎
默认情况下,面向 Embedder 打包的 Flutter 引擎假定以 JIT 模式运行;JIT 引擎携带的 VM 与 AOT 快照不兼容。构建 AOT 引擎的标准入口是引擎源码里的 gn 脚本:
./flutter/tools/gn --runtime-mode release <必要时指定自定义 target 的标志>
--runtime-mode release:复用 Flutter 的 "release" 模式策略来准备 AOT 引擎。这个策略对第三方 Embedder 而言是可选的,是否采用由 Embedder 作者自行决定。<custom target flags>:这里正是指定自定义 target、sysroot 和工具链等标志的位置。
注意:文中提到的 gn 标志与其他非运行时/配置类选项可以自由组合,例如 --no-lto、--unoptimized 等。这些选项在开发调试阶段特别有用,官方明确鼓励使用。
该调用会产出一个针对宿主架构配置好 AOT 模式的 Flutter 引擎。tools/gn 就是引擎源码树中这个构建入口脚本,第三方 Embedder 通常从引擎源码根目录发起该命令。
三、构建目标架构专用的 gen_snapshot
把 Dart 代码转换成特定架构 AOT 指令的二进制叫做 gen_snapshot。一次成功的 gen_snapshot 调用应当产出前文所述的四个二进制块(VM 堆/指令快照 + Isolate 堆/指令快照)。
3.1 用 --version 验证目标架构
一个特定的 gen_snapshot 只能为一种目标架构生成 AOT 指令。用 --version 标志检查它在为什么架构工作:
$ gen_snapshot --version
Dart VM version: 2.1.1-dev.2.0.flutter-ac1bf656c4 (Thu Jan 17 16:55:19 2019 +0000) on "macos_x64"
末尾字符串表示 宿主/目标 对。上例中宿主是 macos、目标是 x64。如果作者在一台 macOS 机器上,一个为 aarch64 生成指令的 gen_snapshot 会显示类似 "macos_simarm64"。
最简单的做法是选择一个架构最接近 Embedder 目标架构的受支持 Flutter target。例如目标为 armv7 时:
./flutter/tools/gn --android --runtime-mode release
面向 aarch64 的 AOT gen_snapshot 另一个常用调用是:
./flutter/tools/gn --android --runtime-mode release --android-cpu arm64
3.2 架构之外的隐蔽约束:调用约定与对齐
重要:gen_snapshot 生成的 AOT 指令与目标之间,除了目标架构一致外,调用约定(calling convention)和对齐(alignment)也必须一致。复用 Flutter 的构建目标可以保证这些细节都被处理,但匹配 AOT 指令与目标的责任仍在 Embedder 一方。例如:--android --android-cpu arm64 配置的 gen_snapshot 生成的指令,并不完全兼容 iOS 上的 aarch64(尽管目标架构相同)。快照的架构/ABI 与设备架构/ABI 不匹配会在快照加载时被检测到并明确报错。
这一点在 Flutter-engine-operation-in-AOT-Mode.md 的 Notes 一节也有佐证:gen_snapshot 只能为特定架构生成指令(armv7、aarch64、i386、x86-64 各需一个变体);四个工件通常必须同时由同一个 gen_snapshot 变体生成,不能把不同变体产生的指令与数据快照混用;从源码结构看,其根本原因是数据快照(堆内容)与指令的编码强依赖于同一编译器的假设。
四、构建 AOT 快照
生成目标架构的 AOT 指令分两步:先生成与目标架构无关的 kernel 快照(约等于 AST),再把它交给 gen_snapshot 生成四个 AOT 块(约等于机器码)。
4.1 便捷方式:复用 flutter build aot 工具链
iOS 和 Android 的 AOT 模式在构建产物时都要做这一步。如果 Embedder 目标与它们相近,可以直接复用 Flutter 工具链支持的工作流。例如为 Android 类目标生成 aarch64 的四个 AOT 块:
flutter --local-engine <local_engine_configuration> \
--local-engine-host <local_host_engine_configuration> \
build aot --target-platform android-arm64 --release
--local-engine/--local-engine-host:技术上可选,但强烈建议指定。不指定时工具会选用已发布的 Flutter 引擎版本,它可能与你用来准备gen_snapshot的引擎存在细微版本差异,锁定同一版本更安全。详见 Debugging-the-engine.md 中"使用本地引擎运行"一节。
调用结果是在 build/aot 目录生成以下四个二进制块:
| 文件名 | 含义 | 运行时内存权限 |
|---|---|---|
vm_snapshot_data |
VM 快照数据 | 只读 |
vm_snapshot_instr |
VM 快照指令 | 读-执行 |
isolate_snapshot_data |
Isolate 快照数据 | 只读 |
isolate_snapshot_instr |
Isolate 快照指令 | 读-执行 |
4.2 手动方式:先 kernel,再 AOT
若需要直接控制四个块的生成,就必须手动走完整流程。具体标志虽然晦涩但可读性强;必要时可以给 flutter build aot 加 -v 标志,dump 出 Flutter 实际使用的精确标志,再按目标架构修改。
生成 kernel 快照
以下调用会在构建目录生成 kernel_snapshot.dill(执行前先在项目里运行 flutter packages get 拉取全部包依赖):
$FLUTTER_ENGINE_OUT_DIR/dart \
$FLUTTER_ENGINE_OUT_DIR/frontend_server.dart.snapshot \
--sdk-root $FLUTTER_ENGINE_OUT_DIR/flutter_patched_sdk/ \
--strong \
--target=flutter \
--aot \
--tfa \
-Ddart.vm.product=true \
--packages .packages \
--output-dill build/kernel_snapshot.dill \
package:flutter_gallery/main.dart
关键参数解读:
--sdk-root .../flutter_patched_sdk/:使用打补丁后的 Flutter SDK(含dart:ui等 Flutter 扩展库);--target=flutter:以 Flutter 为目标而非原生 Dart;--aot+-Ddart.vm.product=true:按 AOT 产品模式编译(强模式、产品开关);--tfa:启用树形流分析(tree-flow analysis);--output-dill build/kernel_snapshot.dill:输出 kernel(.dill)文件;- 最后一个参数是应用入口(示例用
flutter_gallery,实际换成你的入口文件)。
生成 AOT 快照
得到 kernel_snapshot.dill 后,用如下参数调用 gen_snapshot 产出四个块:
$FLUTTER_ENGINE_OUT_DIR/gen_snapshot \
--causal_async_stacks \
--packages=.packages \
--deterministic \
--snapshot_kind=app-aot-blobs \
--vm_snapshot_data=build/vm_snapshot_data \
--isolate_snapshot_data=build/isolate_snapshot_data \
--vm_snapshot_instructions=build/vm_snapshot_instr \
--isolate_snapshot_instructions=build/isolate_snapshot_instr \
--no-sim-use-hardfp \
--no-use-integer-division \
build/kernel_snapshot.dill
--snapshot_kind=app-aot-blobs:声明要生成应用 AOT 的四个二进制块形式;- 四个
--*_snapshot_*参数分别指定四个输出块的路径; --deterministic:保证输出确定性;--causal_async_stacks:启用因果异步栈;- 两个
--no-*标志并非所有目标都需要,可以按需跳过。拿不准时的标准做法:选择最相似的目标后运行flutter build aot -v,查看 Flutter 使用的标志再作修改。
五、打包 AOT 块
四个 AOT 块必须随应用一起分发,FlutterEngineRun 调用离不开它们。如何打包与分发由 Embedder 自行决定。运行时这些块要映射进地址空间,并遵守以下权限约束:
vm_snapshot_data:只读(Read-Only);vm_snapshot_instr:读-执行(Read-Execute);isolate_snapshot_data:只读(Read-Only);isolate_snapshot_instr:读-执行(Read-Execute)。
映射的生命周期由 Embedder 负责:只要 FlutterEngine 在运行并保持存活,这些映射就必须持续有效,不能提前解除。
作为对照,官方平台是这样做的(见 Flutter-engine-operation-in-AOT-Mode.md):iOS 因为引擎无法在运行时把页标记为可执行,把四个块编译进 App.framework 这样的动态库中,通过 kDartVmSnapshotData 等符号引用;Android 则把四个块直接打进 APK,由 libflutter.so 自行映射并保证可执行。第三方 Embedder 若希望走"单动态库"路线,也可以把快照打包成一个 ELF 动态库交给引擎(见下一节的现代 API)。
六、配置引擎进入 AOT 模式
在传给 FlutterEngineRun 的 FlutterProjectArgs 结构体中,提供以下八个选项:
vm_snapshot_data:只读 VM 快照映射的指针;vm_snapshot_data_size:VM 快照映射的大小;vm_snapshot_instructions:读-执行 VM 指令映射的指针;vm_snapshot_instructions_size:VM 指令映射的大小;isolate_snapshot_data:只读 isolate 快照映射的指针;isolate_snapshot_data_size:isolate 快照映射的大小;isolate_snapshot_instructions:读-执行 isolate 指令映射的指针;isolate_snapshot_instructions_size:isolate 指令映射的大小。
至此 Flutter 引擎即运行在 AOT 模式。
在源码中可以逐项核对这些字段的语义。embedder.h 中的注释与文档一一对应:
vm_snapshot_data(第 2586 行):"This buffer must be mapped in as read-only"——必须只读映射;vm_snapshot_instructions(第 2594 行):"must be mapped in as read-execute"——必须读-执行映射;isolate_snapshot_data/isolate_snapshot_instructions(第 2602、2610 行):同样分别要求只读与读-执行;- 四个
*_size字段注明:如果对应指针是一个符号引用(如从动态库 dlopen 得到的符号地址),可以传0——引擎会自行解析块的大小。
此外 embedder.h 还导出 FlutterEngineRunsAOTCompiledDartCode(void),供 Embedder 查询当前引擎是否运行在 AOT 编译的 Dart 代码之上。
补充:ELF 数据源 API(FlutterEngineCreateAOTData)
从 embedder.h 的当前定义看,除了直接传四个裸缓冲指针,稳定 API 还提供了一条更简洁的 AOT 数据入口:
/// AOT data source type.
typedef enum {
kFlutterEngineAOTDataSourceTypeElfPath
} FlutterEngineAOTDataSourceType;
typedef struct {
FlutterEngineAOTDataSourceType type;
union {
/// Absolute path to an ELF library file.
const char* elf_path;
};
} FlutterEngineAOTDataSource;
typedef struct _FlutterEngineAOTData* FlutterEngineAOTData;
配合 FlutterEngineCreateAOTData(source, &data_out) / FlutterEngineCollectAOTData(data) 使用:把四个块打成一个 ELF 动态库(可参照 iOS 的做法),Embedder 只需提供绝对路径。embedder.cc 中该函数的实现会检查当前确实处于 AOT 模式、加载 ELF 并从中取出 vm_isolate_data / vm_isolate_instrs 等符号,再通过 PopulateAOTSnapshotMappingCallbacks(embedder.cc)把四个快照的映射回调注入引擎设置。同文件第 2095 行附近的逻辑也明确了互斥约束:*_snapshot_* 裸缓冲指针与 aot_data 二者只能提供其一,同时提供会返回 "Multiple AOT sources specified" 错误。若走 aot_data 路线,映射生命周期由引擎代管,Embedder 不再需要自己维持映射存活。
七、检查清单
- 引擎是用
./flutter/tools/gn --runtime-mode release(或等价的自定义 target/sysroot/工具链组合)构建的 AOT 引擎; gen_snapshot --version显示的目标架构、调用约定、对齐与设备完全一致(同架构不同 ABI 不算一致);- 四个块由同一个
gen_snapshot变体一次性生成,未混用不同变体; - 打包后在目标设备上映射权限正确:两个 data 块只读、两个 instr 块读-执行;
- 映射在
FlutterEngine存活期间一直有效(或改用 ELF 数据源 API 交给引擎管理); FlutterProjectArgs的八个vm_snapshot_*/isolate_snapshot_*字段全部正确填充后调用FlutterEngineRun,引擎即以 AOT 模式运行。
参考文档:Custom-Flutter-Engine-Embedding-in-AOT-Mode.md、Flutter-engine-operation-in-AOT-Mode.md、Debugging-the-engine.md;核心实现见 embedder.h 与 embedder.cc。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00