一、APK结构分析:知己知彼
做包体积优化,首先得知道APK里到底装了啥。我见过不少团队,上来就一顿操作猛如虎,结果发现压缩了半天,最大的那个文件根本没动过。
APK本质上就是个ZIP压缩包。你可以用任何解压工具打开它。里面主要包含这些部分:
- classes.dex — 你的Java/Kotlin代码编译后的字节码
- resources.arsc — 编译后的资源索引表
- res/ — 原始资源文件(图片、布局等)
- lib/ — Native动态库(.so文件)
- META-INF/ — 签名信息
- AndroidManifest.xml — 清单文件
核心观点:优化前先做一次"体检"。用Android Studio自带的Analyze APK功能,或者用命令行工具aapt2 dump,看看哪个文件占大头。
我个人习惯,拿到一个APK第一件事就是拖进Android Studio的Profile or Debug APK窗口。一眼扫过去,如果lib/目录占了60%以上,那说明动态库是主要矛盾。如果res/占了40%,那图片资源就是突破口。
举个例子,我之前接手过一个项目,APK有80MB。团队一直以为是图片太多,结果我一分析,发现classes.dex有35MB,lib/有28MB。图片反而只有10MB。方向完全错了。
二、资源混淆:AndResGuard实战
资源混淆,说白了就是把资源文件的名字改短。比如 activity_main.xml 变成 a.xml,ic_launcher.png 变成 b.png。这样resources.arsc和ZIP目录里的字符串就短了,整体体积就小了。
AndResGuard是微信团队开源的工具。它的原理不是简单的重命名,而是重新打包整个APK,同时保持资源ID的映射关系不变。
配置方式
// 在项目根目录的build.gradle中
apply plugin: 'AndResGuard'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
}
}
// 在app的build.gradle中
andResGuard {
mappingFile = null
use7zip = true
useSign = true
keepRoot = false
whiteList = [
"R.mipmap.ic_launcher",
"R.drawable.splash_bg"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif"
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.21'
}
}
避坑指南:我曾经遇到过一个问题——混淆后某个第三方SDK的图片找不到了。原因是那个SDK通过资源ID的字符串名来加载图片,而混淆后名字变了。解决方案是把这些资源加入白名单。
效果怎么样?一般来说,资源混淆能减少APK体积5%~15%。具体取决于你的资源文件数量。如果你的项目有上千个资源文件,效果会更明显。
三、代码混淆:ProGuard与R8
代码混淆不仅仅是安全层面的考虑,它对包体积的影响也很大。ProGuard和R8做的事情有三件:
- 压缩(Shrink) — 删除未使用的代码
- 优化(Optimize) — 简化代码逻辑
- 混淆(Obfuscate) — 重命名类、方法、字段为短名称
R8是Google在Android Gradle Plugin 3.4.0之后默认使用的工具。它比ProGuard更快,而且优化效果更好。
R8配置示例
// 在gradle.properties中启用R8
android.enableR8=true
android.enableR8.libraries=true
// 在build.gradle中配置
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
你想想看,一个类名叫 com.example.app.data.repository.UserRepositoryImpl,混淆后变成 a.a.a。这省下来的字节数可不是一星半点。
注意:混淆后一定要保留mapping文件。否则线上崩溃日志里看到的全是a.a.a,根本没法定位问题。我习惯每次发版都把mapping文件归档到Git仓库里。
在实际项目中,代码混淆通常能减少DEX文件体积的20%~40%。但前提是你的代码里没有太多反射调用。如果有反射,记得加 -keep 规则。
四、动态库裁剪:别把整个厨房搬过去
动态库(.so文件)是APK体积的大头之一。很多第三方SDK会提供全平台的so文件,包括arm64-v8a、armeabi-v7a、x86、x86_64等。但实际上,你的用户可能只需要其中一两个。
我见过最夸张的一个项目,so文件占了APK的70%。原因是集成了一个AI推理库,它把Windows、Linux、macOS的so都打包进来了。嗯,这显然是个配置问题。
裁剪策略
| 策略 | 说明 | 体积减少 |
|---|---|---|
| 只保留arm64-v8a | 2023年后新设备基本都是64位 | 约50% |
| ABI分包 | 按CPU架构分发不同APK | 约60%~70% |
| 动态库瘦身 | 用strip命令移除调试符号 | 约10%~30% |
// 在build.gradle中指定ABI
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
}
// 或者使用strip命令手动瘦身
// 在CMakeLists.txt或Android.mk中配置
target_link_libraries(native-lib
${log-lib}
${jnigraphics-lib}
)
// 构建后执行strip
// $NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-strip \
// --strip-debug --strip-unneeded libnative-lib.so
个人经验:我曾经用strip命令把一个3MB的so文件瘦身到2.1MB,减少了30%。而且功能完全不受影响。因为strip只是移除了调试信息,不影响运行时逻辑。
五、ABI分包:精准投放
ABI分包是Google Play推荐的做法。它的核心思想是:不同CPU架构的用户下载不同的APK。arm64-v8a的用户不需要下载armeabi-v7a的so文件。
在Android App Bundle出现之前,我们需要手动配置多个APK。现在有了AAB,Google Play会自动帮你做这件事。
配置方式
// 使用Android App Bundle(推荐)
android {
bundle {
abi {
enableSplit = true
}
}
}
// 或者使用传统多APK方式
android {
splits {
abi {
enable true
reset()
include 'arm64-v8a', 'armeabi-v7a', 'x86_64'
universalApk false
}
}
}
效果有多明显?我参与的一个项目,原始APK是45MB。使用ABI分包后,arm64-v8a版本的APK只有28MB。用户下载时间从原来的15秒缩短到8秒。转化率提升了大概3个百分点。
核心观点:包体积优化的本质是"精准交付"。用户不需要的东西,就不要塞进APK里。资源混淆、代码混淆、动态库裁剪、ABI分包,都是在做同一件事——只给用户他真正需要的那部分。
最后说一句,优化不是一锤子买卖。我建议每个版本发布前都做一次APK分析,看看体积变化趋势。如果某个版本体积突然增加了,那肯定是有东西被误加进来了。及时发现问题,比事后补救要轻松得多。