10、包体积优化:资源混淆与去冗余、动态库按需加载、R8/ProGuard深度配置
包体积优化这件事,说实话,我早年做项目时并不太在意。那时候手机存储还比较紧张,用户装个App要掂量半天。后来我接手过一个直播App,安装包直奔80MB,用户反馈说「你这App比游戏还大」。嗯,从那以后,我就把包体积优化当成必修课了。
今天聊的三个方向——资源混淆与去冗余、动态库按需加载、R8/ProGuard深度配置——是我在实际项目中反复打磨过的。每个点都能砍掉几MB甚至十几MB,而且不影响功能。咱们一个一个说。
资源混淆与去冗余
资源文件是包体积的「隐形杀手」。你想想看,一张没用的图片、一段冗余的字符串,积少成多就是几MB。我见过一个项目,光drawable文件夹就占了15MB,里面三分之一是设计师迭代时废弃的素材。
资源去冗余,说白了就是删掉没用的东西。Android Studio自带的Lint就能做这件事。我个人习惯在每次发版前跑一次Lint检查,把Unused Resources清理掉。命令很简单:
./gradlew lint
然后在 build/reports/lint-results.html 里找到Unused Resources列表,手动删除。但手动删除太慢了,我建议用 shrinkResources 配合 minifyEnabled 自动处理:
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
这里有个坑——shrinkResources 只能移除未被代码引用的资源。如果资源是通过 getIdentifier() 动态引用的,它不会自动识别。我曾经因为这个踩过坑,线上版本某个图标突然消失了,排查了半天才发现是动态引用的资源被误删了。
res/raw/keep.xml 里显式声明要保留的资源:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/keep_me,@layout/also_keep_me" />
资源混淆则是另一回事。它把资源文件名从 ic_launcher.png 缩短成 a.png,从而减小resources.arsc文件的大小。Android官方没有直接提供资源混淆工具,但我们可以用微信开源的 AndResGuard。配置起来很简单:
apply plugin: 'AndResGuard'
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'
}
}
运行 ./gradlew resguardRelease 就能生成混淆后的APK。我实测过,一个20MB的APK,资源混淆后能降到17MB左右,效果很明显。
动态库按需加载
动态库(.so文件)是包体积的大头。一个完整的FFmpeg库动辄十几MB,如果全量打包进APK,用户下载时就得等半天。我做过一个视频编辑App,最开始把armeabi-v7a、arm64-v8a、x86、x86_64四个架构的so库全打进去了,APK直接飙到50MB。
按需加载的核心思路是:只打包当前设备需要的架构。具体做法有两种:
- APK分包:在
build.gradle里配置abiFilters,只保留主流架构:
android {
defaultConfig {
ndk {
abiFilters "arm64-v8a", "armeabi-v7a"
}
}
}
这样打包出来的APK只包含arm架构的so库,x86设备虽然不能直接安装,但Google Play会通过APK Split自动分发对应架构的版本。如果你在国内市场分发,可以单独打一个x86包。
- 动态下载:把so库放到服务器上,App启动时根据设备架构动态下载。我曾在项目中用过这个方案,效果不错,但要注意两点:
- 下载时机要选好,别在用户流量紧张时下载
- 下载完成后要校验MD5,防止so库被篡改
代码示例:
public class SoLoader {
private static final String BASE_URL = "https://cdn.example.com/libs/";
public static void loadSo(String soName, String md5) {
String abi = getAbi();
String url = BASE_URL + abi + "/" + soName;
File soFile = new File(context.getFilesDir(), soName);
// 下载并校验MD5
downloadFile(url, soFile);
if (!checkMd5(soFile, md5)) {
throw new SecurityException("MD5 mismatch");
}
// 加载so库
System.load(soFile.getAbsolutePath());
}
}
R8/ProGuard深度配置
R8是ProGuard的升级版,从AGP 3.4开始成为默认的代码混淆和压缩工具。说白了,它的作用就是:删掉没用的代码、混淆类名和方法名、优化字节码。我见过一个项目,开启R8后APK从30MB降到了22MB,效果立竿见影。
但R8的配置是个技术活。配置太松,效果不明显;配置太紧,容易出问题。我总结了一套「安全配置」:
# 保留所有公共API
-keep public class * {
public *;
}
# 保留序列化类
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留反射调用的类
-keep class com.example.reflection.** { *; }
# 保留JNI方法
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留枚举
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
这里有个关键点——keep规则要精准。我见过有人图省事,直接写 -keep class **,结果所有类都没被混淆,包体积几乎没变。正确的做法是:只保留那些被反射、JNI、序列化等机制引用的类,其他类交给R8自由处理。
R8还有一个隐藏功能——资源优化。它会把资源文件中的字符串常量合并、删除重复的字符串。比如 R.string.app_name 和 R.string.app_title 如果值相同,R8会合并成一个。这个功能默认是开启的,不需要额外配置。
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
知识体系总览
下面这张图是我整理的包体积优化知识体系,涵盖了资源、动态库、代码三个维度。你可以对照着检查自己的项目:
最后说一句,包体积优化不是一锤子买卖。我建议每次发版前都跑一遍优化流程,把新增的资源、代码、so库都检查一遍。久而久之,你就会形成习惯,看到APK体积异常增长时,能快速定位到问题。
嗯,今天就聊到这儿。如果你在配置R8时遇到奇怪的崩溃,或者资源混淆后某个页面显示异常,欢迎随时交流。这些坑我都踩过,能帮你省不少时间。