Flutter Android 包体积优化:ABI 拆分、资源收缩与 AAB 验证
已经能稳定打包和上线 Flutter Android 版本后,下一轮最容易拖慢发布节奏的问题,往往不是签名或渠道,而是包体积开始失控:新增一个图片组件、接一个监控 SDK、补几组字体文件,AAB 体积就会上去,低端机安装时间也会变长。很多团队这时会先去调 minifyEnabled,但如果没有基线、没有分层定位、没有发布口径,最后只能得到一堆“好像小了一点”的结论,下一次又从头排查。
这篇文章只解决一个核心问题:怎么把 Flutter Android 包体积治理做成可复用流程。适用对象是已经具备日常构建、签名和发版能力的团队;交付物是一套从 size baseline、R8 与 shrinkResources、ABI 拆分到 AAB 验证的步骤、配置、命令和复盘方法。本文不讨论 iOS,也不讨论首帧性能本身;如果你还没把发版链路走顺,先看这篇站内文章:Flutter Android 发布前别只会点 Build:签名、AGP/JDK 兼容与 AAB 验证清单。
适用场景、环境版本与边界
这套方法适合下面几种情况:
- Flutter Android 版本已经能稳定产出 release APK 或 AAB,但最近几次发版体积持续上涨。
- 你需要区分“Dart 代码变胖”“资源变多”“Native 库变多”到底是谁在推高下载体积。
- 团队已经用了 flavor、CI 或多渠道配置,希望把包体积核对并入发布检查,而不是靠人工猜。
本文示例按两档环境说明:
- Flutter stable 通道的 Android release 构建流程,命令以
flutter build为主。 - Android Gradle Plugin 8.12/8.13 与 9.0+ 分开看待:8.12/8.13 想用优化后的资源收缩,需要显式打开
android.r8.optimizedResourceShrinking=true;9.0+ 在开启shrinkResources时会自动套用这条链路。
边界也要先说清楚:
- 如果你的体积主要来自视频、离线地图、模型文件或大批量业务素材,R8 只能帮你裁掉代码和无引用资源,解决不了资产本身过大。
- 如果项目依赖反射、动态类加载、序列化框架或某些三方插件,开 shrink 后必须补 keep rules,否则体积变小了,功能也可能被裁坏。
- 如果最终发布目标是 Google Play,真正影响用户下载体积的是 Play 根据设备拆分后的交付结果,不是你本地看到的单个上传包大小。
先建立基线:别在没有指标时谈优化
包体积治理第一步不是改配置,而是先拿到可对比的指标。Flutter 官方提供了 size analysis,先对当前 release 产物做一次基线记录:
flutter clean
flutter pub get
flutter build appbundle --release --analyze-size
执行后要记录三类信息:
- 终端里输出的整体体积摘要。
- 构建目录里生成的
*-code-size-analysis_*.json文件。 - 本次提交对应的 commit、flavor、构建参数和依赖版本。
如果你还需要核对分 ABI 后的 APK 结果,再补一条命令:
flutter build apk --release --split-per-abi
然后看 build/app/outputs/flutter-apk/:
build/app/outputs/flutter-apk/
app-armeabi-v7a-release.apk
app-arm64-v8a-release.apk
app-x86_64-release.apk
这一步的目标不是立刻把数字压到最低,而是建立“谁变大了”的证据。后面每次改动都要回到同样的命令和目录做验证,否则你根本不知道体积下降是来自代码收缩、资源收缩,还是只是因为依赖缓存和构建条件不同。
步骤一:先把 release 收缩链路配正确
很多 Flutter 项目只有 debug 配置长期被反复验证,release 配置却一直靠默认值撑着。真正要做包体积优化,先把 Android release 的收缩链路写成显式配置。
android/app/build.gradle.kts 可以先整理成下面这个最小骨架:
android {
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
如果你的项目还停留在旧模板里的 proguard-android.txt,建议先切到 proguard-android-optimize.txt,否则你打开了压缩,却把优化能力关掉了。AGP 8.12/8.13 还可以在 gradle.properties 里显式打开优化后的资源收缩:
android.r8.optimizedResourceShrinking=true
配置完不要急着宣布完成,先跑一次 release 构建并看日志:
flutter build appbundle --release -v > build-release.log 2>&1
接着确认日志里至少能看到 release shrink 相关任务执行过,重点不是背任务名,而是确认这条链路真的被跑到了,而不是因为变体名、flavor 或脚本覆盖导致 release 仍然按未收缩路径构建。
步骤二:按来源拆体积,别把所有问题都甩给 R8
有了基线后,下一步不是继续调一堆 keep rules,而是按来源拆问题。一个常见误区是看到 AAB 变大,就立刻怪到 Dart 代码;实际上 Flutter Android 体积通常有三层来源:
- Dart AOT 产物和 Flutter engine 相关部分。
- Android
res/、字体、图片、未使用语言资源等静态资源。 - 第三方原生依赖带进来的
.so、AAR 资源和 metadata。
最稳的做法是把分析结果和项目目录对起来看:
lib/ # Dart 业务代码
assets/ # 图片、字体、配置文件
android/app/src/ # AndroidManifest、res、keep rules
如果 size analysis 显示 Dart 部分上升,先回头查最近是不是引入了大体积库、自动生成代码、国际化文案或图标字体。若主要增量在资源层,优先检查 assets/ 和 Android res/;如果增量集中在 native library,继续查是不是某个 SDK 一次性带入了多 ABI、调试符号或并未使用的能力模块。
这一步要形成结论句,而不是只记数字。例如:
- “本次比上版多 5.8 MB,其中 3.9 MB 来自新增地图 SDK 的 arm64 so,Dart 仅增加 400 KB。”
- “本次 AAB 只多 1.2 MB,但安装包分析里图片资源上涨 38%,主要来自未压缩的启动图和三套重复 banner。”
只有把体积归因说清楚,后面的配置和代码修改才不会跑偏。
步骤三:用 ABI 拆分和资源收缩处理大头
当问题确定落在 native 库和资源层时,收益最大的一般不是再写更多混淆规则,而是先做 ABI 与资源治理。
如果你的交付场景需要 APK 分发,比如私有渠道、测试包或企业内部分发,--split-per-abi 是第一条该固定下来的命令:
flutter build apk --release --split-per-abi
这样每个设备只拿到自己需要的 ABI 包,不会把 armeabi-v7a、arm64-v8a、x86_64 全塞进一个通用 APK。对 Google Play 来说,AAB 也会按设备拆分交付,但你仍然要本地核对拆分后的结果,因为很多团队的问题不在“能不能拆”,而在“引入了不该引入的 ABI”。
资源侧要做三件事:
- 打开
isShrinkResources = true,让未引用资源随 release 一起裁剪。 - 审核
assets/目录里是否混入了设计稿原图、重复深色/浅色资源、测试 json 或旧多语言文件。 - 对 Android 原生资源使用明确的
resourceConfigurations或等价策略,避免把根本不支持的语言和密度资源一起打进包里。
一个实际的核对动作是:同样的业务代码,分别构建一次“未开 shrink”的 release 和“已开 shrink”的 release,对比构建日志里的资源裁剪摘要、APK/AAB 产物大小和安装后的主要页面回归结果。只有体积指标和页面测试都过,才说明这一步是有效优化,而不是危险裁剪。
步骤四:把 keep rules、符号和图标当成精细化治理
做到这里,包体积通常已经能降下一截,但真正容易反复踩坑的,是“为了保功能把所有东西 keep 住”,最后又把体积涨回去。比较稳的做法是把 keep rules 控制在“只保运行必需项”,再把符号和图标单独治理。
先看 proguard-rules.pro。如果你为了图省事加过这类规则:
-keep class com.example.** { *; }
-keep class io.flutter.plugins.** { *; }
要尽快缩到最小边界。大面积 keep 的后果很直接:R8 不敢删,资源收缩也会跟着保守。排查方法不是靠感觉,而是每次只放开一个插件或序列化入口,然后跑 release 构建、功能测试和日志验证。
Flutter 侧还要注意两个容易被忽略的点:
- 图标字体和字体文件常常在没有感知的情况下变大,尤其是直接整包引入 icon font 或多字重字体时。
--split-debug-info不只影响可读性和符号管理,也能帮助减少最终代码体积;但你必须把符号目录纳入发布物归档,否则上线后崩溃回溯会断链。
可以把 release 命令收敛成下面这样:
flutter build appbundle --release --analyze-size --split-debug-info=build/symbols
然后把 build/symbols、mapping 文件、size analysis json 一起归档到同一次发布记录里。这样你下次看到体积波动或线上崩溃时,排查链路不会断。
步骤五:把体积验证并入 CI 和发布清单
包体积优化如果只靠一次手工排查,很快会反弹。更稳的办法是把验证写进 CI 或发布脚本,让每次 release 都输出同一组指标。
一个可执行的最小检查顺序可以是:
flutter pub get
flutter test
flutter build appbundle --release --analyze-size --split-debug-info=build/symbols
CI 里至少保留这些产物:
app-release.aabmapping.txt或等价符号映射build/symbols/*-code-size-analysis_*.json- 体积对比记录,例如本版与上版的 AAB 大小、arm64 APK 大小、核心资源目录大小
指标怎么定不要拍脑袋,先从“超阈值提醒”开始。例如:
- AAB 比上一稳定版上涨超过 8% 就阻塞提审。
- arm64 APK 比上一版上涨超过 5 MB 时必须补原因说明。
- 新增大体积 SDK 时,PR 描述里必须附 size analysis 差异截图或关键数字。
这类规则的价值不在于绝对准确,而在于把“体积变化”变成每次发布都能看见、能解释、能复盘的工程信号。
验证、避坑与失败边界
验证不能只看包大小,还要看功能和日志。建议至少过下面四项:
flutter test和关键 Widget/集成测试正常。- release 包在真机安装、启动、登录、核心页面进入都正常。
- release 日志里没有因为资源缺失、类被裁掉、反射失败引发的新异常。
- Play Console 或内部测试渠道看到的下载体积趋势,和本地 AAB / split APK 的变化方向一致。
常见避坑点有这些:
- 把 debug 构建结果拿来讨论体积,结论基本没有参考价值。
- 只看上传包大小,不核对设备实际下载大小和安装后验证。
- 因为一次 shrink 出问题,就把 keep rules 放大到整包保留,后续再也收不回来。
- 忘了 flavor 差异,结果 production 开了 shrink,staging 没开,CI 指标全失真。
失败边界也要保守:如果某个 SDK 在 release shrink 后功能不稳定,不要一口气把所有优化关掉;先限定在该插件、该能力或该变体上回退,同时保留其余已经验证通过的优化项。
复盘清单
每次 Flutter Android 包体积治理结束后,建议按下面的清单复盘:
- 这次增长主要来自 Dart、资源还是 native library?
- 哪一条配置、命令或目录核对最先暴露问题?
- 哪些 keep rules 是本次新增的,它们是否有明确责任人和回收条件?
- size analysis、mapping、symbols 是否都跟本次 release 一起归档?
- 下次发版前,是否要把某个资源目录、字体策略或 SDK 引入规则前置到代码评审?
如果你已经在做多环境发布,可以把这里的体积检查再和这篇站内文章连起来看:Flutter Android 多环境配置实战:flavor、dart-define-from-file 与 CI 构建检查清单。前者解决“每个环境怎么稳定构建”,本文解决“稳定构建之后,怎么让发布包持续可控”。