Flutter 引擎 AOT 快照体积对比方法:用 gen_snapshot 指令尺寸报告做字节级归因
本文介绍如何在 Flutter 引擎(Flutter Engine)中准备一份“AOT 快照体积差异”的表格化汇总:通过 flutter build aot 的 --verbose 输出捕获 gen_snapshot 调用,借助 --print-instructions-sizes-to 选项把每个方法编译后的指令字节数导出为 JSON,再用对比工具对“变更前/变更后”两份 JSON 做差分。读完后,你将掌握一套可复制的流程,用于量化某次 Dart 代码改动、或某次引擎(gen_snapshot 二进制)改动,究竟为 AOT 产物增加或减少了多少字节,并定位到具体库与具体方法。
适用前提与前置条件
这套方法的目标是“比较两个 AOT 快照(AOT snapshot)的大小差异”,适用场景包括:
- 评估某次 Dart 代码变更(例如新增 API、调整库实现)对 AOT 产物体积的影响;
- 评估引擎侧改动——重新构建
gen_snapshot二进制——后对产物体积的影响。
使用该方法前,需要满足两个前提(来自 Comparing-AOT-Snapshot-Sizes.md 的原文说明):
- 宿主机上已完成 Flutter 引擎环境的搭建(见 Setting-up-the-Engine-development-environment.md),并在下文所有命令中用环境变量
FLUTTER_ENGINE指向该引擎源码目录。 - 如果要配合本地构建的引擎做测量,
flutter build还支持--local-engine与--local-engine-host两个标志(说明见 Debugging-the-engine.md 的“Running a Flutter app with a local engine”一节)。
流程一:捕获 flutter build aot 中的 gen_snapshot 调用
第一步是为应用构建 AOT 快照,即执行 flutter build aot,并传入 --verbose 标志。目的不是完成构建本身,而是要在冗长输出中找到工具实际发起的 gen_snapshot 调用,以便稍后加上额外选项重跑它。
需要重跑的核心选项是 --print-instructions-sizes-to:它让 gen_snapshot 把“各方法指令尺寸”的汇总写入一个 JSON 文件。在本文流程中,该 JSON 文件的路径用 $SUMMARY_LOCATION 表示。
文档给出的调用示例如下(注意保留原有参数、只增加 --print-instructions-sizes-to 一项):
$FLUTTER_ENGINE/out/host_debug_unopt/gen_snapshot \
--print-instructions-sizes-to=$SUMMARY_LOCATION \
--causal_async_stacks \
--packages=.packages \
--deterministic \
--snapshot_kind=app-aot-blobs \
--vm_snapshot_data=build/aot/vm_snapshot_data \
--isolate_snapshot_data=build/aot/isolate_snapshot_data \
--vm_snapshot_instructions=build/aot/vm_snapshot_instr \
--isolate_snapshot_instructions=build/aot/isolate_snapshot_instr \
build/aot/app.dill
几点解读(结合仓库内其他引擎文档):
- 示例以
out/host_debug_unopt构建目录下的gen_snapshot为例,实际应使用你本地引擎构建出的那个二进制; build/aot/app.dill是 AOT 编译链路中的 kernel 输入,产物是四个快照文件(VM/Isolate 的 data 与 instructions 各两份)。Custom-Flutter-Engine-Embedding-in-AOT-Mode.md 也说明了gen_snapshot一次成功调用会生成这四个二进制 blob;--deterministic保证快照内容可复现,这对“前后两次测量可比”很重要;- 文档同时提醒:AOT 编译在调用
gen_snapshot之前还有一步 kernel 编译(见 Custom-Flutter-Engine-Embedding-in-AOT-Mode.md 的“Generating the kernel snapshot”一节)。这意味着改了 Dart 代码后,必须重新走完整的flutter build aot来刷新 kernel,不能只重跑gen_snapshot。
执行完示例命令后,把 $SUMMARY_LOCATION 指向的 JSON 文件保存为 before.json,作为基线。
流程二:制造差异并重新测量
基线确定后,制造你关心的差异,二选一(文档原文如此描述):
- 修改 Dart 代码,引入你想观察效果的变更;或
- 重新构建引擎,得到一个更新后的
gen_snapshot二进制。
然后执行两轮重新构建:
- 重新运行
flutter build aot。文档特别强调这一步的重要性:因为 AOT 编译在gen_snapshot之前存在 kernel 编译步骤,只有重跑完整构建,app.dill才会反映你的代码改动; - 重新运行
gen_snapshot调用(与流程一相同的命令,包括--print-instructions-sizes-to),并把这次生成的 JSON 保存为after.json。
至此,你手上有了两份结构相同的尺寸汇总文件:before.json(变更前)与 after.json(变更后)。
流程三:运行 compare_size.dart 生成差异表格
对比工具 compare_size.dart 接受两个 JSON 文件,输出方法级的字节差值汇总表。文档给出的调用形式为:
$FLUTTER_ENGINE/out/host_debug_unopt/dart-sdk/bin/dart \
before.json \
after.json
即使用引擎构建目录中随附的 Dart SDK 来运行该脚本。需要如实说明:在当前仓库内检索不到 compare_size.dart 文件本身,文档将其作为工具引用(它随 Dart SDK/引擎工具链分发,并非本仓库中的源码),因此在实际执行前需要在你的工具链环境中确认该工具的位置与名称。
如何读懂输出:方法级 Diff 与 Stub 行
文档给出的示例输出形如(节选):
+------------+----------------------------------------------------------+--------------+
| Library | Method | Diff (Bytes) |
+------------+----------------------------------------------------------+--------------+
| dart:async | new ZoneSpecification.from | +2136 |
| dart:async | runZoned | +1488 |
| dart:async | new _CustomZone | +927 |
...
| | [Stub] Type Test Type: class 'PopupMenuEntry' | -128 |
| | [Stub] Type Test Type: class '_SyncIterator@0150898' | -128 |
| dart:io | new Directory | -211 |
| dart:io | new Link | -211 |
+------------+----------------------------------------------------------+--------------+
Total change +24036 bytes.
表格的三列含义:
| 列 | 含义 |
|---|---|
| Library | 方法所属库(如 dart:async、dart:io),Stub 行该列为空 |
| Method | 具体方法名;[Stub] 前缀表示编译器生成的桩代码(示例中为 Type Test 类型检查桩),@0150898 之类的后缀是实例可区分的类型哈希 |
| Diff (Bytes) | 该方法指令字节数相对基线的差值,正数为增大、负数为缩小 |
表尾的 Total change 给出整体 AOT 指令体积的净变化(示例为 +24036 字节)。读表时的两个要点:
- 正负行可能同时出现(示例中
dart:async系列大量增长,而部分 Type Test 桩与dart:io构造函数在缩小),需要同时看净变化与结构性变化,才能判断体积增长的来源是“方法本身膨胀”还是“生成的桩/类型检查结构变化”; - 差值是“方法级指令字节”层面的归因,与最终
.so/ELF 文件在磁盘上的大小不完全等同(还受链接、打包等影响),适合做改动间相对比较,而非绝对体积承诺。
gen_snapshot 参数与 AOT 产物背景
从示例调用中还能看到一组反复出现的关键参数,值得单独说明:
--snapshot_kind=app-aot-blobs:生成 AOT 应用快照的四个 blob;--vm_snapshot_data/--isolate_snapshot_data/--vm_snapshot_instructions/--isolate_snapshot_instructions分别指定四个输出路径;--causal_async_stacks:开启因果异步栈,属于影响产物内容/体积的编译选项之一——如果你在对比“开关某编译选项”的体积影响,这类开关项就是变量之一;--deterministic:确定性输出,保证两次测量之间差异只来自你刻意引入的变更。
此外,仓库中 Fuchsia 组件的 AOT 构建配置 flutter_dart_component.gni 展示了同一选项在正式构建系统里的用法:其中 gen_snapshot 被以 --deterministic、--snapshot_kind=app-aot-elf、--print-instructions-sizes-to=<stats json> 的方式调用,即引擎构建流程本身也会把指令尺寸汇总落盘为 JSON。可以推断,本文档所描述的对比流程与构建系统内置的统计能力出自同一机制:JSON 汇总由 --print-instructions-sizes-to 产生,差异分析在其之上进行。
关于产物的通用背景,可参阅:
- Custom-Flutter-Engine-Embedding-in-AOT-Mode.md:
gen_snapshot的构建方式、host/target 架构匹配(例如用--version校验二进制对应的宿主/目标平台)、kernel 快照的生成; - Flutter-engine-operation-in-AOT-Mode.md:四个 AOT 产物(VM/Isolate 的 data 与 instructions 快照)的用途,以及“同一次调用生成的四个文件不可混用不同
gen_snapshot变体的产物”等约束。
完整操作流程速查
把三条流程串起来,完整的最小操作序列是:
- 搭建引擎环境,
FLUTTER_ENGINE指向引擎源码目录(参考 Setting-up-the-Engine-development-environment.md); flutter build aot --verbose,从输出中抄录gen_snapshot调用;- 加上
--print-instructions-sizes-to=$SUMMARY_LOCATION重跑该调用,JSON 存为before.json; - 修改 Dart 代码或重建引擎的
gen_snapshot; - 重新执行完整
flutter build aot(刷新 kernel); - 再次重跑
gen_snapshot,JSON 存为after.json; - 用
dart compare_size.dart before.json after.json生成方法级 Diff 表格与Total change。
该流程的价值在于:把“体积变大了”这种笼统结论,落实到“哪个库、哪个方法、变化多少字节”的粒度,为 Dart SDK/框架代码瘦身与引擎编译器调优提供可核查的依据。适用限制同样明确:它依赖本地构建的引擎与 --verbose 构建日志,且 JSON 汇总由 gen_snapshot 在特定编译选项下生成——对比前后应保持除目标变更外其余编译参数(如 --causal_async_stacks)一致,否则差分中会混入无关变量。
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 StartedRust0627
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