Flutter 包体积优化实战:从 175MB 到 105MB

最近在做公司的一个评估类 App,用 Flutter 写的,跑起来没问题,但每次打包出来发给测试同学,APK 都大得离谱。终于抽时间认真搞了一次包体积优化,最终从 175MB 压缩到 105.1MB,减少了将近 40%,记录一下整个过程。

先搞清楚包大的根本原因

我们这个项目依赖很重,光 pubspec.yaml 里就有四十多个包:高德地图、Firebase、视频压缩、PDF 阅读、TTS、相机、图片裁剪......每一个都带着自己的原生 .so 库。Flutter 本身编译出来的 Dart 代码其实不大,真正把包撑大的是这些原生库。

但在分析依赖之前,我先看了一眼 android/app/build.gradle,发现了一个让我有点哭笑不得的配置:

gradle 复制代码
buildTypes {
    release {
        minifyEnabled false
        shrinkResources false
    }
}

minifyEnabled falseshrinkResources false------release 包完全没有做任何压缩和混淆。这就相当于把所有调试信息、未使用的代码、未使用的资源全部打进去了。这一个问题,就能直接贡献 20~40% 的体积。

第一步:开启 R8 压缩

build.gradle 改成这样:

gradle 复制代码
buildTypes {
    release {
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    debug {
        minifyEnabled false
        shrinkResources false
    }
}

minifyEnabled true 会开启 R8 编译器,它会做三件事:移除未使用的 Java/Kotlin 代码、内联优化、压缩字节码。shrinkResources true 则会把未被代码引用的 Android 原生资源(drawable、layout 等)也一并清理掉。

第二步:写 ProGuard 规则,否则必崩

开启 R8 之后,第一次构建直接报错了:

kotlin 复制代码
Missing class com.google.android.play.core.splitcompat.SplitCompatApplication
Missing class com.google.android.play.core.splitinstall.SplitInstallManager
...

这是 Flutter 内嵌代码引用了 Google Play 动态组件加载的类,但我们 App 根本用不到这个功能,R8 找不到这些类就直接报错。好在 R8 会自动生成一个 missing_rules.txt 文件告诉你该加什么规则,把里面的内容复制到 proguard-rules.pro 里加上 -dontwarn 就解决了。

这也提醒了我,开启 R8 之后必须要有一份完整的 ProGuard 规则文件。于是我针对项目所有依赖逐一梳理,写了一份覆盖以下内容的规则:

  • Flutter 核心类、注解、签名信息
  • 高德地图 SDK(com.amap.api.**
  • Firebase 全系列,包括 Crashlytics 的堆栈符号保留
  • Retrofit、OkHttp、Gson(接口层用的多)
  • 生物识别、相机、图片裁剪、WebView 等各插件的 Java 层
  • 枚举、Serializable、Native 方法等通用规则

写 ProGuard 规则这步不能偷懒,漏掉任何一个被 R8 误裁的类,上线后就是线上崩溃。

第三步:去掉 32 位支持

原来的配置是同时打 armeabi-v7aarm64-v8a,也就是 32 位和 64 位各一份原生库全都塞在一个 APK 里。

我们这个 App 面向企业内部工作人员使用,minSdk = 24(Android 7.0,2016 年),能跑 Android 7 的手机基本全是 64 位的。

查了一下数据:2026 年,32 位设备(armeabi-v7a)在 Android 活跃设备中占比已不足 2%,且几乎全是印度、东南亚市场的极低价功能机。

果断只保留 64 位:

gradle 复制代码
ndk {
    abiFilters "arm64-v8a"
}

所有原生 .so 库直接砍掉一半,这一步又能省 15~25%。

第四步:清理没人用的资源文件

扫了一遍 assets/images/ 目录,发现有 7 个图片文件在整个 Dart 代码库里找不到任何引用:

  • test.png(204K)------一看就是开发时随手放进去的测试图
  • guide1_16_8.pngguide2_16_8.pngguide3_16_8.png(各 167~171K)
  • guide1_16_9.pngguide2_16_9.pngguide3_16_9.png(各 75~99K)

这些是之前做引导页功能时留下的,后来功能废弃了,图片却没有一起删。在 lib/ios/android/plugin/ 四个目录全局搜索,确认无任何引用后直接删除,合计释放约 1MB

最终效果

优化项 预计收益
开启 R8(minify + shrink) 减少 20~40%
去掉 armeabi-v7a 减少 15~25%
删除无用图片资源 减少约 1MB

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 175 MB → 优化 105.1 MB ↓ 39.9 % 175\text{MB} \xrightarrow{\text{优化}} 105.1\text{MB} \quad \downarrow 39.9\% </math>175MB优化 105.1MB↓39.9%

三项叠加,实际从 175MB 压缩到 105.1MB ,减少了将近 70MB,降幅接近 40%。

后续还能继续优化的方向

这次优化做的都是"低垂的果实"------改配置、删冗余,基本没有什么风险。如果后续想继续压缩,还有几个方向可以往下挖。

1. 把 PNG 图片转成 WebP 格式(预计再减 3~5MB)

项目里目前剩余的 PNG 图片,除了带透明通道的 icon 类图片,其他的截图、背景图、示意图都可以转成 WebP。WebP 无损压缩比 PNG 小 26%,有损压缩比 JPEG 小 25~35%,且 Android 4.0+ 原生支持,完全无兼容顾虑。

批量转换命令(需要安装 cwebp):

bash 复制代码
# 安装
brew install webp

# 批量转换 assets/images 下所有 PNG
for f in assets/images/*.png; do
  cwebp -q 85 "$f" -o "${f%.png}.webp"
done

转完之后记得把代码里 .png 的引用改成 .webp,并把旧 PNG 文件删掉。

2. 用 --analyze-size 找出最重的依赖(精准定位)

目前只是凭经验判断哪些依赖重,实际上 Flutter 提供了专门的包体积分析工具,能生成一个可视化的 Treemap 报告,精确到每个 Dart 包、每个原生库占了多少字节:

bash 复制代码
fvm flutter build apk --analyze-size

构建完成后会在 build/ 目录生成 *-code-size-analysis_*.json,运行 dart devtools 打开 App Size 工具上传这个文件,就能看到类似下图这样的占比分布,哪个库最重一目了然。

3. 按需加载------把不常用功能改成懒加载

观察了一下我们的依赖,有几个库体积不小但使用频率很低:

  • pdfx:PDF 阅读功能,只有查看合同时才用,带着 PDFium 原生库约 8MB
  • video_compress:视频压缩,只有录制后上传才用,MediaCodec 原生库约 4MB
  • flutter_tts:语音播报,只有特定场景才用

这类功能可以考虑延迟初始化,在第一次真正用到时再初始化插件实例,而不是 App 启动时全部初始化。虽然这不会减少安装包体积,但能减少启动内存占用和首帧时间,属于性能优化的范畴。

4. 检查高德 SDK 是否重复引入

目前 build.gradle 里手动引入了高德的三个 SDK:

gradle 复制代码
implementation 'com.amap.api:3dmap:8.1.0'
implementation 'com.amap.api:location:6.4.3'
implementation 'com.amap.api:search:9.4.0'

同时 pubspec.yaml 里的 Flutter 插件 amap_flutter_mapqq_amap_flutter_location 自身也会依赖这些 SDK。Gradle 依赖仲裁机制虽然不会打进两份代码,但版本可能不一致,导致同时加载了两个版本的类。

建议把 build.gradle 里手动引入的这三行删掉,完全交给 Flutter 插件的传递依赖来管理,保持版本统一,也减少 Gradle 解析时间。

5. 考虑用 App Bundle(AAB)替代 APK 上架

如果后续需要上架国内应用市场或 Google Play,强烈建议改用 App Bundle 格式:

bash 复制代码
fvm flutter build appbundle --obfuscate --split-debug-info=build/symbols/

AAB 的好处是应用市场会根据设备配置(CPU 架构、屏幕密度、语言)按需分发,用户实际下载的体积会比通用 APK 小 3050%。我们现在的包是 105MB,用 AAB 分发后用户下载可能只有 6070MB。


几点经验

1. 先看构建配置再分析依赖。 很多时候包大不是因为依赖多,而是因为 release 包根本没做压缩。这种低级问题可能藏了很久没人发现。

2. R8 的 missing_rules.txt 是救命稻草。 开启混淆后第一次构建报错很正常,R8 自己会生成需要补充的规则,按它说的加就行,不要慌。

3. 去掉 32 位之前先确认目标用户群体。 如果是面向大众消费者的 App,还是要保留 armeabi-v7a;如果是内部工具类 App,直接去掉没什么顾虑。

4. 定期扫一下 assets 目录。 资源文件很容易随着功能迭代变成"孤儿文件",没有工具会提醒你,只能靠手动或者写脚本定期清理。

相关推荐
zeqinjie4 小时前
Skills-Flutter 内测泄漏审核
前端·flutter·app
UnicornDev6 小时前
【Flutter x HarmonyOS 6】魔方计时APP——记录页面的UI设计
flutter·ui·华为·harmonyos·鸿蒙
用户5052372099157 小时前
Dart 3.x 完全指南:Records / Patterns / Class Modifiers(2026)
flutter
折翅鵬1 天前
Flutter Accordion 完全指南:从折叠面板入门到高级样式自定义
flutter
jiejiejiejie_1 天前
Flutter for OpenHarmony 渐变色UI设计实战:LinearGradient与RadialGradient深度应用
flutter·ui
xmdy58661 天前
Flutter+开源鸿蒙实战|城市共享驿站智能存取系统 Day1 项目初始化+架构分层+多端适配+全局状态基座
flutter·开源·harmonyos