App 瘦身实战:图片、so 库、资源压缩,安装包体积减小 40%
用户不会下载一个 180MB 还不知道好不好用的 App。
在弱网、低端机、小存储设备上,包体积每大 6MB,转化率大约掉 1% ------这不是玄学,是 Google Play 和国内大厂反复验证过的结论。
本文不讲"理论上可以瘦",只讲实际能砍掉 40% 体积的一套组合拳。
一、先搞清楚:你的包里到底什么最占地方
动手之前,先拆包看数据。
1. APK Analyzer 拆包
Android Studio → Build → Analyze APK,打开你的 release APK/AAB,你会看到类似这样的分布:
| 类别 | 典型占比 | 说明 |
|---|---|---|
lib/ (so 库) |
35%--50% | 各 ABI 的 native 库 |
res/ (资源) |
20%--30% | 图片、布局、动画、字体 |
classes.dex |
15%--25% | 编译后的代码 |
assets/ |
5%--15% | 模型文件、H5、数据库、SDK 内置资源 |
resources.arsc |
3%--5% | 资源索引表 |
META-INF/ |
2%--3% | 签名信息 |
不同项目分布差异巨大 ,但 80% 的体积问题集中在 lib/ + res/ + assets/ 三个目录。
2. 输出体积报告(CI 自动化)
bash
# 解压 APK 后统计各目录大小
unzip -l app-release.apk | awk '{print $4}' | sed 's|/.*||' | sort | uniq -c
或者更直观的脚本:
python
import zipfile, os
from collections import defaultdict
def analyze_apk(apk_path):
size_by_dir = defaultdict(int)
with zipfile.ZipFile(apk_path) as z:
for info in z.infolist():
top_dir = info.filename.split('/')[0] if '/' in info.filename else '(root)'
size_by_dir[top_dir] += info.file_size
total = sum(size_by_dir.values())
for dir_name, size in sorted(size_by_dir.items(), key=lambda x: -x[1]):
print(f"{dir_name:20s} {size/1024/1024:8.2f} MB {size/total*100:5.1f}%")
print(f"{'TOTAL':20s} {total/1024/1024:8.2f} MB")
analyze_apk("app-release.apk")
二、so 库:体积大户,砍 ABI 立竿见影
1. 只保留 arm64-v8a
2024--2026 年的现实:
- arm64-v8a 覆盖 >95% 的活跃设备
- armeabi-v7a 只剩老千元机(Android 8 以下)
- x86 / x86_64 只有模拟器和 Chrome OS,真机用户为零
arduino
// app/build.gradle
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a'
}
}
}
如果你担心老设备,至少做 App Bundle:
ini
android {
bundle {
abi {
enableSplit = true
}
}
}
AAB 上传 Play 商店后,用户只下载自己设备的 ABI,so 库体积直接 ÷3--4。
2. 去掉不需要的 so
很多 SDK 自带 5--8 个 so,你只用了其中 1 个功能。
arduino
# 查看 APK 里有哪些 so
unzip -l app-release.apk | grep ".so$"
常见问题:
| 情况 | 处理 |
|---|---|
| FFmpeg 全编进去了 | 只编你用的 decoder/encoder |
| OpenCV 完整 so 12MB | 用官方 opencv-mobile 裁剪版(~2MB) |
| SDK 自带 stub so | 联系 SDK 方要 lite 版,或 exclude |
| 两个 SDK 带了同一份 libc++_shared | 只保留一份,其余 packagingOptions exclude |
arduino
android {
packagingOptions {
exclude 'lib/armeabi-v7a/libunused.so'
exclude 'lib/x86/lib*.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
}
}
3. 用 LTO + Linker GC 缩小 so
bash
# CMakeLists.txt
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -flto -Oz -ffunction-sections -fdata-sections")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto -Oz -ffunction-sections -fdata-sections")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections")
strip 符号表(release 自动做,确认一下):
arduino
android {
buildTypes {
release {
ndk {
debugSymbolLevel 'none' // 不上传 debug symbol 就不生成
}
}
}
}
4. 真实收益
| 操作 | 节省 |
|---|---|
| 只留 arm64-v8a | 40%--60% of lib/ |
| App Bundle ABI split | 同上,但用户侧自动生效 |
| 去掉未用 so | 2--10MB |
| LTO + strip | 10%--20% of each so |
一个 45MB 的 so 目录,做完以上通常能压到 18--22MB。
三、图片:最大的"隐形肥胖"
1. PNG → WebP(最核心一步)
WebP 比 PNG 小 25%--35% ,有损模式下更小,且支持透明度。
bash
# 批量转换
for f in $(find app/src/main/res -name "*.png"); do
cwebp -q 80 "$f" -o "${f%.png}.webp"
rm "$f"
done
Android Studio 自带批量转换:
右键 res/ → Convert to WebP → 选质量 75--85 → 预览对比 → 确认。
注意:
- minSdk < 18 的设备不支持 WebP(无透明度有损),但 2026 年这个用户群 <1%
- 如果必须兼容,用
webp有损 +pngfallback 放在drawable-v18/
2. 删重复 & 过大图片
常见浪费:
| 问题 | 解决 |
|---|---|
| 同一图标 3 套(logo.png / logo@2x.png / logo@3x.png) | 只留一套最高分辨率,让系统缩放 |
| 引导页背景 3MB 的 JPG | 压缩到 200KB,用户肉眼看不出区别 |
| 占位图用 1080p | 缩到 400×400 |
| 9-Patch 图片未优化 | 用 Android Studio 9-Patch 工具重新划边 |
压缩工具推荐:
- TinyPNG / TinyJPG(在线,效果好)
- ImageOptim(Mac 本地)
- cwebp 命令行批量
- Sharp(Node.js 流水线)
3. 矢量图替代位图
所有图标、简单插画,用 VectorDrawable:
ini
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#FF0000" android:pathData="M12,2L2,22h20L12,2z"/>
</vector>
收益:一个 24×24 的 PNG 需要 5 套(mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi)= ~15KB,矢量版 = 2KB,任意缩放。
适合矢量的:图标、Logo、简单装饰、箭头、按钮背景
不适合的:照片、复杂渐变插画、人脸
4. 动态下发大图
启动引导页、活动弹窗、运营 Banner 这些大图,别打进包里:
less
// 首次启动从 CDN 下载到本地缓存
Coil.imageLoader(context).enqueue(
ImageRequest.Builder(context)
.data("https://cdn.xxx.com/splash_v2.webp")
.target(imageView)
.build()
)
本地缓存一份,下次直接用,更新时服务器换图就行,不用发版。
5. 真实收益
| 操作 | 节省 |
|---|---|
| PNG → WebP | 25%--35% of res/drawable |
| 删冗余分辨率 | 10%--20% of res/ |
| 矢量替代图标 | 50%--80% of icon 体积 |
| 大图动态下发 | 2--15MB 不等 |
res/ 目录通常能砍 30%--50%。
四、资源压缩:R8 + shrinkResources + 更狠的招
1. 代码压缩(R8)
java
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
minifyEnabled 做:
- 删除未使用类/方法
- 混淆缩短名称
- 优化字节码
shrinkResources 做:
- 删除
res/中代码未引用的资源 - 注意 :只删
res/,不删assets/
2. 防 shrinkResources 误删
xml
<!-- res/raw/keep.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/splash_bg,@layout/legacy_fallback,@raw/config_json" />
或者 proguard-rules.pro:
kotlin
-keep class com.xxx.model.** { *; }
-keepclassmembers class * implements android.os.Parcelable { *; }
3. 资源混淆(AndResGuard)
微信开源方案,把资源名缩短 + 7zip 重打包:
ini
// build.gradle
apply plugin: 'AndResGuard'
andResGuard {
mappingFile = null
use7zip = true
useSign = true
keepRoot = false
whiteList = [
"R.drawable.icon",
"R.string.app_name"
]
}
效果:
- 资源名
activity_main_splash_background→0x00a1 resources.arsc缩小- 7zip 压缩率高于 ZIP
- 整体再省 3%--8%
4. 删无用语言资源
arduino
android {
defaultConfig {
resConfigs "zh", "zh-rCN", "en"
}
}
默认会打包所有 SDK 支持的语言(fr/es/de/ja/ko/ru/...),砍掉不需要的能省 1--3MB。
5. 删无用 densities
arduino
android {
defaultConfig {
resConfigs "xxhdpi", "xxxhdpi"
}
}
如果你只留最高两档,系统会向下缩放。代价是低端机多一点点 GPU 缩放开销,换来包体积减少 5--10MB。
五、assets/:最容易被忽略的肥肉
1. 审计 assets
bash
du -sh app/src/main/assets/*
常见问题:
| 内容 | 大小 | 处理 |
|---|---|---|
| 内置 H5 离线包 | 10--30MB | 拆包,首启下载 |
| 机器学习模型 (.tflite) | 5--20MB | 量化 INT8、动态下发 |
| 内置字体 (.ttf) | 3--8MB | 用系统字体或子集化 |
| SDK 内置配置 JSON | 0.5--2MB | 接口下发 |
| 离线地图数据 | 20--100MB | 按需下载 |
2. 字体子集化
你只用了一个字体里的 200 个汉字 + 英文数字?
css
# 用 fonttools 提取子集
pyftsubset font.ttf --text="你好世界ABCDEFG123" --output-file=font-subset.ttf
从 6MB → 30KB。
3. H5 离线包拆包
scss
// 首启时下载核心 H5 包
WorkManager.getInstance(context)
.enqueue(DownloadWebBundleWorker::class.java)
把非核心页面(活动页、帮助中心)放 CDN,核心流程页才内置。
六、Dex 瘦身
1. R8 全开 + 开启优化
arduino
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles 'proguard-android-optimize.txt'
}
}
}
proguard-android-optimize.txt 比 proguard-android.txt 多了优化步骤。
2. 删调试符号 & 堆栈
arduino
android {
buildTypes {
release {
debuggable false
jniDebuggable false
}
}
}
3. 检查依赖树
bash
./gradlew app:dependencies
常见问题:
- 引入了
guava整个库但只用了Strings.isNullOrEmpty - 引入了
gson+moshi+kotlinx-serialization三套 JSON - 引入了
appcompat+androidx.core+legacy-support大量重叠
用哪个删哪个:
arduino
dependencies {
implementation 'com.google.code.gson:gson:2.10.1'
// 删掉 moshi,统一用 gson
}
4. 开启 Dex 压缩(Android 10+)
ini
android {
buildFeatures {
buildConfig = false
}
}
BuildConfig 生成编译时常量,如果不用就关掉。
七、AAB(Android App Bundle):分发侧最后一刀
如果你上架 Google Play,AAB 是必选项,不是可选项。
Play 商店按用户设备动态生成 APK:
| 维度 | 效果 |
|---|---|
| ABI split | 只下 arm64 或 armv7 |
| Screen density split | 只下 xxhdpi |
| Language split | 只下用户语言 |
| Feature modules | 非核心功能动态下发 |
ini
android {
bundle {
language { enableSplit = true }
density { enableSplit = true }
abi { enableSplit = true }
}
}
真实案例 :一个 85MB 的 APK,AAB 分发后用户平均下载 38MB,减少 55%。
国内渠道不支持 AAB?用 腾讯 VasDolly 或 美团 Walle 做多渠道打包,至少把 ABI 和 density 的 split APK 做好,让用户下载对应架构的安装包。
八、实战:从 92MB 到 55MB 的完整记录
某电商 App 的真实瘦身过程:
| 步骤 | 操作 | 体积变化 |
|---|---|---|
| 起点 | 原始 release APK | 92.4 MB |
| ① | ABI 只留 arm64-v8a | 92.4 → 71.2 MB |
| ② | PNG → WebP(全量) | 71.2 → 58.6 MB |
| ③ | R8 + shrinkResources | 58.6 → 54.1 MB |
| ④ | 删未用语言(只留 zh/en) | 54.1 → 52.8 MB |
| ⑤ | 删冗余 densities(只留 xxhdpi+) | 52.8 → 49.3 MB |
| ⑥ | AndResGuard 资源混淆 + 7zip | 49.3 → 46.7 MB |
| ⑦ | assets 大图动态下发 | 46.7 → 43.5 MB |
| ⑧ | 字体子集化 | 43.5 → 42.8 MB |
| ⑨ | 依赖清理(删重复 JSON 库) | 42.8 → 41.2 MB |
| 结果 | 最终 APK | 41.2 MB(↓ 55%) |
AAB 分发后用户平均下载 28MB。
九、防反弹:瘦身不是一次性的
1. CI 卡体积红线
bash
# 构建后检查 APK 大小
APK_SIZE=$(stat -f%z app/build/outputs/apk/release/app-release.apk)
LIMIT=$((55 * 1024 * 1024)) # 55MB 上限
if [ $APK_SIZE -gt $LIMIT ]; then
echo "❌ APK size $(($APK_SIZE/1024/1024))MB exceeds limit 55MB"
exit 1
fi
2. 每次发版对比
bash
# 记录基线
echo "$(date +%Y%m%d) $(git rev-parse --short HEAD) $(stat -f%z app-release.apk)" >> size_history.txt
3. 新 SDK 准入审查
每个新 SDK 引入时检查:
- 带了多少 so?
- 带了多少 res/?
- 有没有 assets/ 大文件?
- 能不能用 lite 版?
十、快速自查清单
✅ abiFilters 'arm64-v8a' 或 AAB ABI split
✅ 全量 PNG → WebP(质量 80)
✅ minifyEnabled true + shrinkResources true
✅ resConfigs 限制语言和 density
✅ 图标矢量化为 VectorDrawable
✅ 大图/字体/H5 动态下发
✅ AndResGuard 资源混淆
✅ 依赖树审计,删重复库
✅ assets/ 逐项审查
✅ CI 体积红线 + 发版对比
最后
包瘦身的本质就一句话:
用户手机里不值得放的东西,就别打进去。
能动态下发的下发,能压缩的压缩,能删的删,能合并的合并。
40% 不是极限------按上面这套做完,大多数 App 能瘦 40%--55% ,AAB 分发后用户侧感知更明显。