Flutter APK 体积144MB到23MB:瘦身实战

  • 发表于
  • flutter

本文记录了一个真实 Flutter 项目(含 MediaKit 视频播放器、QuickJS 引擎、InAppWebView 等重量级插件)从 144MB 优化到 23MB 的全过程,涵盖 AGP 压缩策略、ABI 分包、代码混淆、Dart AOT 分析等多个维度。

背景

项目是一个多功能影视聚合应用,技术栈:

  • Flutter 3.35.0 / Dart 3.9.0
  • MediaKit(libmpv 视频播放)、QuickJS(JS 引擎)、InAppWebView
  • AGP 8.6.1 / Kotlin 2.1.0 / Gradle 8.11.1
  • 目标:通过 OTA 和仓库分发 APK

某次升级 AGP 和 Kotlin 版本后,APK 从 ~25MB 暴涨到 ~47MB——几乎翻倍。排查发现问题并非代码膨胀,而是 一个被忽视的 native library 压缩策略变更

TL;DR 优化效果

阶段APK 大小节省
初始(3 ABI 全包)144.3 MB
单 ABI 构建46.9 MB-97.4 MB
恢复代码混淆44 MB-2.9 MB
GBK 映射表去重42.9 MB-1.1 MB
启用 useLegacyPackaging23.5 MB-19.4 MB
highlight 按需导入23.1 MB-0.4 MB

第一刀:单 ABI 构建(-97MB)

Flutter 默认构建包含多个 CPU 架构的 native 库。对于 APK 直接分发场景,没必要把三种架构塞进同一个包。

问题

方案:通过  --target-platform  指定单 ABI

但  --target-platform  只控制 Flutter 引擎和 Dart AOT 产物的架构,不影响第三方插件 AAR 中打包的 native 库(如 libmpv.so)。需要在  build.gradle  中配合 ABI filter 和 packaging excludes:

为什么需要  packaging.jniLibs.excludes  ndk.abiFilters  只控制 CMake/ndk-build 编译的产物。像 media_kit 这类通过 JAR 分发预编译 .so 的插件,其多架构库会通过 Gradle 依赖解析进入 APK,不受  abiFilters  约束。

第二刀:恢复代码混淆(-2.9MB)

问题

项目原先通过  gradle.properties  配置混淆:

升级 AGP 后此配置被注释掉,改为依赖构建命令行参数。但常常忘记加  --obfuscate

验证:Flutter Gradle 插件确实读取此属性

查看 Flutter SDK 源码  FlutterPlugin.kt

方案:使用标准 Gradle 属性

比起旧的  extra-gen-snapshot-options ,Flutter 的 Gradle 插件还支持更语义化的属性:

这样无论构建命令是否带  --obfuscate ,混淆都会自动生效。 split-debug-info  配合使用可保留符号映射用于崩溃日志还原。

第三刀:GBK 映射表去重(-1.1MB)

问题

项目中 三个位置 各自嵌入了一份 23,943 条的 GBK-UTF16 映射表:

  • lib/utils/decode_body.dart (24,047 行)
  • lib/utilsv2/decode_body.dart (24,051 行)
  • package:fast_gbk (依赖库自带)

三份映射表在 AOT 编译后占用  libapp.so  约 6MB

方案

统一使用  fast_gbk  包,删除两处嵌入的映射表:

第四刀:关键转折—— useLegacyPackaging (-19.4MB)

这是本文最核心的优化,也是最容易被忽视的。

问题定位

通过  python3 + zipfile  分析 APK 内部压缩情况时,发现所有  .so  文件的压缩率竟然是 100%(即完全未压缩):

37.8MB 的 native 库占了 APK 的 86%,却一字节都没有压缩。

根因

AGP 8.x + minSdkVersion ≥ 23 时,默认行为变为  extractNativeLibs=false

  • .so 文件以**未压缩、页面对齐(16KB aligned)**的方式存入 APK
  • Android 6.0+ 系统可直接从 APK mmap 加载 .so,无需解压
  • 优势:安装后磁盘占用小(不需要 APK + 解压两份),安装速度快
  • 劣势:APK 下载体积显著增大

旧版 AGP 8.3.2 +  minSdkVersion 21  时默认压缩 .so,升级 AGP 8.6.1 +  minSdkVersion  改为  flutter.minSdkVersion (值为 24 ≥ 23)后,默认行为静默改变。

方案

在  build.gradle  中显式启用传统打包:

在  AndroidManifest.xml  中同步声明:

效果

兼容性评估

Android 版本API Level行为兼容性
5.0-5.121-22系统总是解压 .so,忽略  extractNativeLibs
6.0-9.023-28 extractNativeLibs=true  是原生默认行为
10.0+29+两种模式都支持,true 走传统解压路径
  • useLegacyPackaging = true 是 Android 从第一个版本就支持的传统打包方式
  • media_kit 官方在 v1.0.2 CHANGELOG 中明确标注 perf: enable extractNativeLibs
  • 唯一代价:安装后磁盘占用增大(需同时存储 APK 和解压的 .so),对现代设备的 128GB+ 存储不构成问题

选择策略建议

分发方式推荐配置原因
APK 直接分发 / OTA useLegacyPackaging = true 下载体积优先
Google Play AAB可用默认(false)Play Store 有自己的增量分发和压缩机制
内部测试 useLegacyPackaging = true 传输效率优先

第五刀:highlight 按需导入(-0.4MB)

问题

package:highlight  的默认导入方式  import 'package:highlight/highlight.dart'  会注册全部 190 种语言定义,在 AOT 编译中占用 1.3MB。项目只用了 JavaScript 高亮。

方案

附:如何分析你的 Flutter APK 体积

方法一:Flutter 官方  --analyze-size

注意: --analyze-size  不能与  --obfuscate  /  --split-debug-info  同时使用。

会输出一份 JSON 报告,可用 Dart DevTools 可视化:

方法二:Python 脚本分析 APK 压缩状况

上文的  useLegacyPackaging  问题就是用此方法发现的——官方工具只看到 raw size,看不出压缩率:

方法三:解析  --analyze-size  JSON 各包体积

最终 APK 组成

libapp.so 内部各包体积 Top 15

通过  --analyze-size  报告(未混淆版本)递归计算:

包名大小占比说明
package:foxwlr4,554 KB20.2%项目自身代码
package:flutter4,467 KB19.8%Flutter Framework
package:fast_gbk2,259 KB10.0%GBK 编解码映射表
@unknown1,565 KB6.9%生成代码/内部
package:highlight1,329 KB5.9%代码高亮(可优化)
@shared703 KB3.1%共享 stubs
dart:core478 KB2.1%Dart 核心库
package:pointycastle400 KB1.8%加密库(传递依赖)
dart:ui308 KB1.4%UI 引擎绑定
package:flutter_inappwebview295 KB1.3%WebView
package:flutter_localizations287 KB1.3%国际化
package:html266 KB1.2%HTML 解析
dart:io214 KB0.9%IO 库
package:image204 KB0.9%图像处理
package:flutter_html186 KB0.8%HTML 渲染

总结:优化检查清单

  1. 单 ABI 构建--target-platform android-arm + packaging.jniLibs.excludes
  2. 启用 native 库压缩useLegacyPackaging = true + extractNativeLibs="true"
  3. 代码混淆dart-obfuscation=true 写入 gradle.properties
  4. 去除重复数据 — 排查项目中嵌入的大数据表是否与依赖库重复
  5. 按需导入 — 检查 highlight、intl 等包是否加载了不需要的资源
  6. 使用分析工具--analyze-size + Python zipfile 脚本双管齐下
  7. 注意 AGP 升级的副作用 — 版本升级可能静默改变 native 库打包策略

最容易忽视且收益最大的是第 2 点。当你发现 APK 里的  .so  文件没有被压缩时,一行配置就能砍掉近一半体积。


本文基于 Flutter 3.35.0 / AGP 8.6.1 / Gradle 8.11.1 环境编写,数据来自实际项目构建。