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

执行后要记录三类信息:

  1. 终端里输出的整体体积摘要。
  2. 构建目录里生成的 *-code-size-analysis_*.json 文件。
  3. 本次提交对应的 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-v7aarm64-v8ax86_64 全塞进一个通用 APK。对 Google Play 来说,AAB 也会按设备拆分交付,但你仍然要本地核对拆分后的结果,因为很多团队的问题不在“能不能拆”,而在“引入了不该引入的 ABI”。

资源侧要做三件事:

  1. 打开 isShrinkResources = true,让未引用资源随 release 一起裁剪。
  2. 审核 assets/ 目录里是否混入了设计稿原图、重复深色/浅色资源、测试 json 或旧多语言文件。
  3. 对 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.aab
  • mapping.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 构建检查清单。前者解决“每个环境怎么稳定构建”,本文解决“稳定构建之后,怎么让发布包持续可控”。