App 瘦身实战:图片、so 库、资源压缩,安装包体积减小 40%

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 有损 + png fallback 放在 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_background0x00a1
  • 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.txtproguard-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 分发后用户侧感知更明显。

相关推荐
大勇前进3 小时前
Kotlin 协程实战避坑:90% 的人都会踩的生命周期泄漏问题
后端
苏三说技术4 小时前
Spring AI、LangChain4j、AgentScope、Embabel,哪个AI框架更好?
后端
烽学长4 小时前
(附源码)基于Springboot+vue的图书阅读分享系统的设计与实现
java·spring boot·后端
isfox4 小时前
Python 单例模式:一个类只能有一个实例,到底怎么实现?
后端
青山木4 小时前
RocketMQ 入门到原理(三):消息存储原理
java·后端·中间件·架构·rocketmq
小白男神4 小时前
MySQL进阶学习三(视图)
后端·mysql
猿人谷4 小时前
Jev:当 AI 不再生成 Token,而是直接做决策
后端·langchain·aigc
苍何4 小时前
WorkBuddy + 腾讯乐享,原来知识库还能这么用
后端