Flutter 引擎磁盘占用追踪与调试:treemap 分析、devicelab 体积基准与 AOT 快照大小对比
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
本指南围绕 Flutter 仓库中引擎产物(尤其是 Android 平台的libflutter.so、APK/IPA 安装包、icudtl.dat与 VM/isolate 快照)的磁盘占用追踪方法展开。读完本文后,你将掌握:如何通过 CI 产物查看引擎各组件大小的 treemap、如何在本地用binary_size_treemap.sh自行生成 treemap、devicelab 上持续追踪 APK/IPA 体积的基准指标含义,以及如何用gen_snapshot与compare_size.dart做 AOT 快照的逐方法字节级 diff 分析。
为什么需要追踪引擎的磁盘占用
Flutter 引擎的最终产物(libflutter.so、iOS 的Flutter.framework、icudtl.dat、AOT 快照等)直接决定了用户下载包的大小。Flutter 仓库为此提供了一套“从粗到细”的工具链:
- treemap(树状图):把
libflutter.so按符号/组件拆分成面积图,直观看到每个组件对二进制体积的贡献; - devicelab 基准:每次提交都在 CI 上编译 Flutter Gallery 与最小 hello_world 应用,持续上报 APK/IPA 及其中关键产物的压缩/未压缩大小;
- AOT 快照大小对比:在改动 Dart 代码或引擎后,用
gen_snapshot的--print-instructions-sizes-to输出指令大小 JSON,再用compare_size.dart生成“哪个库、哪个方法变大/变小了多少字节”的表格。
这三层工具分别回答三个问题:引擎里什么占地方、最近一次改动让安装包变大还是变小、具体是哪些 Dart 方法让 AOT 代码变大。
Treemap:查看libflutter.so的组件级大小分布
CI 自动生成的 treemap
在 CI 中,每笔提交都会为 release 版本的libflutter.so生成 treemap,图示其中各组件的相对大小。产物上传到对象存储,并从 CI 构建页面链接出来。在构建系统中定位某个 commit 的 treemap 的步骤是:
- 打开 flutter/flutter 仓库 main 分支的 commit 列表;
- 找到要评估的 commit,点击其绿色 checkbox 查看各项 check;
- 找到名为
Linux linux_android_aot_engine的 check,点击details; - 点击
View more details on flutter-dashboard进入 LUCI 构建页面; - 展开名为
launch builds的区域; - 找到类似
Linux Production Engine Drone for ci/android_release_arm64的被触发构建并点开; - 在该构建的
log links区域中找到并点击index.html链接,即可打开 treemap。
本地生成 treemap
不想依赖 CI 时,仓库内置的 binary_size_treemap.sh 可以在本地生成同样的 treemap:
flutter/ci/binary_size_treemap.sh <path/to/libflutter.so> <output_directory>(注意flutter/ci是引擎源码目录内的路径,即本仓库中的 engine/src/flutter/ci。)
从脚本源码看其工作方式:
- 它要求输入是
libflutter.so一类的 ELF 动态库,输出目录即 treemap HTML 的存放位置; - 脚本会定位到引擎构建根目录(
ENGINE_BUILDROOT),然后调用 Dart SDK 附带的二进制体积分析工具 run_binary_size_analysis.py,命令核心为:python3 "$RUN_BINARY_SIZE_ANALYSIS" --library "$INPUT_PATH" \ --destdir "$DEST_DIR" \ --addr2line-binary "$ADDR2LINE" \ --nm-binary "$NM" --jobs 1 --no-check-support - 它按主机系统(macOS 用
darwin-x86_64、Linux 用linux-x86_64)选取 Android NDK 28.2.13676358 中的llvm-addr2line与llvm-nm(见 binary_size_treemap.sh):nm用来枚举符号,addr2line用来把地址区间映射回源码/符号名,从而算出每个组件的体积贡献。因此该脚本需要在完整的引擎构建环境中运行(能取到对应版本的 NDK 工具链)。
treemap 的用途是“定位大头”:当某次提交导致libflutter.so明显变大时,先在 treemap 中看是哪一块区域(图形库、文本渲染、平台通道等)面积增长,再进入下一层工具做细粒度归因。
devicelab 基准:持续追踪 APK/IPA 与引擎产物大小
Flutter 仓库的 dev/devicelab 中有一组针对包体积的基准测试,随每笔提交运行,结果展示在构建仪表盘上。与引擎大小最相关的指标有:
| 指标类别 | 指标名(<suite>__compile/<metric>形式) | 含义 |
|---|---|---|
| Flutter Gallery 的 APK/IPA 总大小 | Android:flutter_gallery_android__compile/release_size_bytesiOS: flutter_gallery_ios__compile/release_size_bytes | 完整 Gallery 应用 release 安装包大小 |
| 最小 hello_world 应用 APK/IPA 大小 | Android:hello_world_android__compile/release_size_bytesiOS: hello_world_ios__compile/release_size_bytes | 剥离业务代码后的“纯 Flutter”基线体积 |
icudtl.dat大小 | 压缩在 APK 内:hello_world_android__compile/icudtl_compressed_bytes未压缩: hello_world_android__compile/icudtl_uncompressed_bytes | ICU 数据文件的体积 |
release 模式libflutter.so大小 | 压缩在 APK 内:hello_world_android__compile/libflutter_compressed_bytes未压缩: hello_world_android__compile/libflutter_uncompressed_bytes | 引擎主体动态库的体积 |
| VM 与 isolate 快照(数据 + 指令) | 压缩在 APK 内:hello_world_android__compile/snapshot_compressed_bytes未压缩: hello_world_android__compile/snapshot_uncompressed_bytes | AOT 快照产物的体积 |
从源码结构看这些指标的产生位置:devicelab 的编译基准统一由 perf_tests.dart 驱动。其中_compileApp方法负责 release 构建并上报总大小(perf_tests.dart):
- Android:执行
flutter build apk --release --target-platform=android-arm --tree-shake-icons --split-debug-info=infos/,然后取build/app/outputs/flutter-apk/app-release.apk的文件大小作为release_size_bytes; - iOS/macOS:执行
flutter build ios|macos --release --tree-shake-icons --split-debug-info=infos/,找到 app bundle 后执行tar -zcf build/app.tar.gz <app>,以该 tarball 大小模拟 IPA 体积(源码注释说明这样做的目的是“验证 Dart 快照格式与数据布局的变化不会改变压缩体积”,同时近似 IPA 大小); - Windows:取生成的
.exe文件大小。
APK 内产物的逐项大小则由getSizesFromApk得到:它运行unzip -v <apk>解析清单,提取lib/armeabi-v7a/libflutter.so、lib/armeabi-v7a/libapp.so与assets/flutter_assets/NOTICES.Z的压缩/未压缩大小,产出libflutter_compressed_bytes、libapp_compressed_bytes等指标(perf_tests.dart)。iOS 侧则从Frameworks/Flutter.framework/Flutter与App.framework/App读取未压缩大小(perf_tests.dart)。
使用建议:
- 判断“整体是否变胖”看
release_size_bytes两条曲线(Gallery 反映真实世界、hello_world 反映引擎基线); - 判断“引擎还是 Dart 侧变胖”看
libflutter_*与snapshot_*的相对变化; - 判断 ICU 数据变化看
icudtl_*。
对比 AOT 快照大小:定位到具体 Dart 方法
当snapshot_*指标出现明显变化,或你主动修改了 Dart 代码/引擎编译器、想确认对 AOT 代码量的影响时,仓库提供了逐方法粒度的对比流程,完整步骤记录在 Comparing-AOT-Snapshot-Sizes.md。前提条件是该机器已完成引擎开发环境搭建(见 Setting-up-the-Engine-development-environment.md),下文用FLUTTER_ENGINE指代引擎源码根。
第 1 步:带--verbose构建 AOT 产物
对应用执行flutter build aot并加--verbose。目的是从日志里找到gen_snapshot的实际调用参数,然后补一个额外选项--print-instructions-sizes-to重新运行。如果用的是本地引擎构建,flutter build还需加上--local-engine与--local-engine-host标志(参见 Debugging-the-engine.md 中“用本地引擎运行 Flutter 应用”一节)。
重新运行gen_snapshot的示例(SUMMARY_LOCATION是输出的 JSON 摘要文件路径):
$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各关键参数含义:
--print-instructions-sizes-to:把每个方法生成的指令大小按库/方法写入 JSON;--snapshot_kind=app-aot-blobs:生成 AOT 应用的四个快照产物(VM/isolate 的 data 与 instructions);- 其余
--vm_snapshot_*/--isolate_snapshot_*参数指定四个输出文件路径,build/aot/app.dill是输入 kernel 文件; - 其余参数(
--packages、--deterministic、--causal_async_stacks)应与你从flutter build aot --verbose日志中看到的原始调用保持一致,这样才能公平对比。
将SUMMARY_LOCATION文件另存为before.json。
第 2 步:引入变更后重新生成
二选一:修改 Dart 代码(观察业务代码改动的影响),或重新构建引擎以获得新的gen_snapshot(观察编译器/运行时改动的影响)。
注意:再次执行flutter build aot这一步不可跳过,因为 AOT 编译在gen_snapshot之前还有一次 kernel 编译步骤(参见 Custom-Flutter-Engine-Embedding-in-AOT-Mode.md 中“生成 kernel 快照”一节),需要刷新 kernel 输入。重新运行gen_snapshot调用,把结果存为after.json。
第 3 步:运行compare_size.dart生成差异表格
用引擎构建输出中的 Dart SDK 运行比较工具:
$FLUTTER_ENGINE/out/host_debug_unopt/dart-sdk/bin/dart \ before.json \ after.json输出是按“库 / 方法 / 字节差”排序的表格,正数表示方法变大,负数表示变小,末尾给出总量变化。示例输出(截取自 Comparing-AOT-Snapshot-Sizes.md):
+------------+----------------------------------------------------------+--------------+ | Library | Method | Diff (Bytes) | +------------+----------------------------------------------------------+--------------+ | dart:async | new ZoneSpecification.from | +2136 | | dart:async | runZoned | +1488 | | dart:async | new _CustomZone | +927 | ... | | [Stub] Type Test Type: class 'PopupMenuItem' | -128 | | dart:io | new Directory | -211 | +------------+----------------------------------------------------------+--------------+ Total change +24036 bytes.从示例可以看出两点:表格同时列出普通方法与[Stub](如类型测试 stub),因此能区分“真实代码变多”和“运行时 stub 增减”;每行给出精确的字节差,配合总量行即可回答“这次改动到底让 AOT 代码净增多少”。
工具链小结
| 场景 | 工具 | 入口 |
|---|---|---|
看libflutter.so里什么组件占体积 | treemap | CI 构建页index.html或 binary_size_treemap.sh |
追踪提交间 APK/IPA 与libflutter.so、快照、icudtl.dat体积趋势 | devicelab 基准 | perf_tests.dart 上报的release_size_bytes、libflutter_*、snapshot_*、icudtl_*指标 |
| 定位 AOT 快照变大的具体 Dart 方法 | gen_snapshot --print-instructions-sizes-to+compare_size.dart | Comparing-AOT-Snapshot-Sizes.md |
三层工具自上而下构成完整的体积回归排查路径:先用 devicelab 曲线发现异常,再用 treemap 圈定引擎内的大头组件,最后用 AOT 快照对比把变化归因到具体方法,从而支撑对引擎磁盘占用的持续监控与问题定位。
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考