背景

Flutter 项目打 Android Release 包时,APK 体积往往远大于预期。一个通用 APK 动辄几十上百 MB,对分发(蒲公英/侧载)和用户下载体验都不友好。

本文记录一套组合优化方案,目标是 把实际分发给用户的安装包做小、做专

现象

典型的包体积问题表现为:

  • 单个 APK 同时包含 armeabi-v7aarm64-v8ax86_64 等多套 ABI 的 .so 库
  • 未使用的代码和资源原样打进 Release 包
  • 调试/测试用架构也被带进正式分发包

优化方案

核心思路是 四个组合拳

步骤 作用 实施位置
1. R8 混淆 + 资源裁剪 移除无用代码和资源 build.gradle
2. 限制目标 ABI 去掉 x86_64(模拟器才用) build.gradle
3. --split-per-abi 拆包 按 CPU 架构打独立 APK 构建命令
4. 分发优先选 arm64-v8a 主力用户包 发布脚本

1. Release 启用 R8 混淆和资源裁剪

android/app/build.gradlerelease 构建类型中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// android/app/build.gradle
android {
buildTypes {
release {
// 代码混淆:移除未使用的类和方法
minifyEnabled true
// 资源裁剪:移除未引用的资源文件
shrinkResources true
// R8 混淆规则文件
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}

这一步的效果:

  • 移除未使用的 Java/Kotlin 代码
  • 裁掉无用的 drawable、layout 等资源
  • 对第三方 SDK 也有效(只要它们的 ProGuard 规则允许)

2. 限制 Release 目标 ABI

默认情况下 Flutter 会把所有 ABI 的 .so 库都打进 APK。实际上:

ABI 适用场景 Release 是否需要
arm64-v8a 绝大多数 Android 真机(64位ARM) ✅ 主力
armeabi-v7a 老旧 32 位 ARM 设备 ✅ 备用
x86_64 模拟器 ❌ 去掉

android/app/build.gradle 中覆盖 ABI 过滤:

1
2
3
4
5
6
7
8
9
10
11
// android/app/build.gradle.kts
afterEvaluate {
// 只在非 split 模式下生效(split 模式由 flutter build apk 控制)
if (!android.splits.abi.isEnable) {
android.defaultConfig.ndk {
abiFilters.clear()
// 只保留真机需要的两个 ABI
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a"))
}
}
}

注意:这里用 afterEvaluate 而不是直接在 defaultConfig 里写,是因为 Flutter Gradle 插件在 apply 阶段可能会覆盖 ABI 配置。afterEvaluate 确保在插件完成后覆盖,不会互相打架。

3. Release 构建改为按 ABI 拆包

构建命令加上 --split-per-abi

1
flutter build apk --release --split-per-abi

输出目录 build/app/outputs/flutter-apk/ 会生成三个独立的 APK:

1
2
3
app-armeabi-v7a-release.apk
app-arm64-v8a-release.apk
app-x86_64-release.apk

比一个「大而全」的通用包,单个 APK 体积显著缩小。用户下载时只需要对应架构的包。

4. 分发时优先选 arm64-v8a

发布脚本中,Android APK 的选取逻辑:

1
2
3
4
5
6
7
# 优先取 arm64-v8a(主流真机)
APK_FILE=$(ls -t build/app/outputs/flutter-apk/*arm64-v8a*release*.apk 2>/dev/null | head -1)

# 找不到再取 armeabi-v7a(老旧设备兼容)
if [ -z "$APK_FILE" ]; then
APK_FILE=$(ls -t build/app/outputs/flutter-apk/*armeabi-v7a*release*.apk 2>/dev/null | head -1)
fi

原因:

  • 现在市面上绝大多数 Android 真机都是 64 位 ARM
  • arm64-v8a 是实际分发的主力包
  • x86_64 只给模拟器和特殊测试环境

完整构建脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
set -e

echo "=== Flutter Android Release Build ==="

# 1. 清理旧构建
flutter clean

# 2. 按 ABI 拆分构建 Release APK
flutter build apk --release --split-per-abi

# 3. 列出产物
echo ""
echo "=== Build Output ==="
ls -lh build/app/outputs/flutter-apk/

# 4. 取主力包
MAIN_APK=$(ls -t build/app/outputs/flutter-apk/*arm64-v8a*release*.apk | head -1)
echo ""
echo "=== Main Distribution APK ==="
echo "$MAIN_APK ($(du -sh "$MAIN_APK" | cut -f1))"

如果上架应用商店:优先 AAB

对 Google Play 等应用商店分发,最佳实践是打 AAB:

1
flutter build appbundle --release

商店会根据设备 ABI 自动分发对应版本,不需要人工拆包和挑包。

验证

本地构建验证:

1
2
flutter build apk --release --split-per-abi && \
ls -lh build/app/outputs/flutter-apk/

预期结果:

  • 看到按 ABI 拆分的独立 APK
  • 单个 APK 比不拆分前明显更小
  • 构建日志中包含 --split-per-abi

注意事项

  1. .so 库才是体积大头:如果项目依赖了 IM、音视频、WebView 等原生 SDK,so 体积本身就不小。当前方案解决的是”构建方式导致的额外膨胀”,不是把原生依赖变没。

  2. 修改 ABI 要理解 Flutter Gradle 插件行为:不要在不理解插件行为的前提下随意同时改 abiFilters--split-per-abi,否则可能某些 ABI 不生成或行为冲突。

  3. 后续还能做的:检查是否有未使用但仍保留的原生插件、调试资源是否误入 Release、图片资源是否可以继续压缩。

参考