做过Android开发的人大多遇到过这种情况:功能还没加几个,APK已经膨胀到八九十兆,老板拿着竞品二十多兆的包来问为什么差距这么大。APK体积不仅影响用户下载转化率,还直接关系到应用市场的推广成本。想让包瘦下来,核心抓手就两个方向:一是资源压缩,把没用的图片、布局、字符串清理干净;二是代码混淆,ProGuard不仅能让代码难以被逆向,还能顺带剔除未使用的类和方法,瘦身和安全一举两得。这篇文章把两条线都讲透,配好可直接使用的配置示例。

先弄清楚APK里面到底装了什么
动手瘦身之前,建议先用Android Studio的Analyze APK功能(Build菜单下)看一眼包内构成。一般来说,一个中型项目的APK大致由这些部分组成:classes.dex(编译后的Java/Kotlin代码)、res目录(图片、布局等资源)、resources.arsc(资源索引表)、lib目录(各种abi的so库)、assets目录。不同类型的应用占比差异很大,图片密集型应用里res可能占一半以上,带NDK的应用里lib目录反而最大。
分析清楚占比后才能对症下药。如果res占比高,优先做资源压缩和图片格式优化;如果dex占比高,重点放在ProGuard混淆和依赖治理上;如果lib占比高,就要考虑abiFilters裁剪。盲目优化往往事倍功半,比如包里全是WebP图片了还拼命压缩图片,收益微乎其微。
资源压缩:清理无用资源与图片优化
资源清理的第一步是开启官方的shrinkResources。这个功能需要配合代码混淆一起使用,因为它的原理是先跑一遍代码收缩分析,找出哪些资源在代码里被引用,剩下的才会被判定为无用资源。在build.gradle里这样配置:
android {
buildTypes {
release {
minifyEnabled true // 开启代码混淆,shrinkResources依赖它
shrinkResources true // 开启资源压缩
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}注意一个常见坑:通过反射获取资源id的资源(比如getIdentifier方式加载的图片)会被误判为无用资源然后删掉,运行时直接崩溃。解决办法是在res/raw目录下建一个keep.xml文件,显式声明保留:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/icon_*,@string/app_name_*"
tools:shrinkMode="safe" />图片优化方面收益通常最明显。几条实用建议:优先使用WebP格式替代PNG和JPG,WebP在有损压缩下体积能小百分之三十左右,且Android 4.0以上就支持;大图考虑用tinypng或ImageOptim二次压缩;针对不同dpi只提供必要密度的图片,很多团队的做法是只保留xxhdpi一套,让系统自动缩放;矢量图(VectorDrawable)能替代大量简单图标,一张SVG矢量图往往只有几KB。
ProGuard混淆:瘦身与防逆向双重收益
ProGuard做的事情概括起来是四个词:压缩(shrink)、优化(optimize)、混淆(obfuscate)、预校验(preverify)。压缩阶段会移除未被引用的类、字段和方法,这一步对体积的贡献不小,尤其是引入了大型SDK却只用了少部分功能的时候。混淆阶段把类名、方法名替换成a、b、c这样的短名字,既缩小了dex体积,也让反编译后的代码可读性大幅下降。
但混淆规则写不好会翻车。最典型的问题是运行时崩溃报ClassNotFoundException或者NoSuchMethodException,根源是把反射调用的类混淆掉了。基础配置中,四大组件、自定义View这些由系统反射实例化的类必须保留:
# 保留四大组件和Application,系统通过反射访问它们
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
# 保留自定义View,布局XML会反射实例化
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
}
# 保留JNI回调的native方法所属类
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留泛型和注解信息,序列化库依赖它们
-keepattributes Signature
-keepattributes *Annotation*另外强烈建议开启混淆后的mapping文件留存。ProGuard混淆后会在build/outputs/mapping目录生成mapping.txt,它记录了混淆前后的名称映射。线上出现崩溃时,用retrace工具配合mapping文件才能把堆栈还原成可读的类名。每次发版务必归档对应版本的mapping文件,否则线上崩溃日志基本没法排查。如果接入了第三方SDK,记得阅读其接入文档的混淆说明,主流SDK都提供了现成的proguard规则,直接拷贝到proguard-rules.pro里即可。
so库裁剪与其他进阶手段
带NDK功能的应用,lib目录往往是大头。每个ABI的so库都是完整一份,armeabi-v7a和arm64-v8a两套加起来体积翻倍。可以用abiFilters只保留主流架构:
android {
defaultConfig {
ndk {
// 只打包主流架构,如需兼容很老的设备可加回armeabi-v7a
abiFilters "arm64-v8a", "armeabi-v7a"
}
}
}还有几个值得做的点:检查依赖树清理重复或无用依赖,运行./gradlew dependencies查看,很多传递依赖悄悄塞进来了用不到的库;启用R8的全量模式可以获得比ProGuard更强的优化;语言资源裁剪,用resConfigs只保留中文和英文,能砍掉Support库自带的几十种语言资源:
android {
defaultConfig {
resConfigs "zh", "en"
}
}最后提醒一点,瘦身要建立可持续的机制。可以在CI流程中接入包体积监控,每次合并代码自动对比APK大小,超过阈值就报警,避免体积不知不觉涨回去。混淆配置改动后务必跑一遍完整的回归测试,重点覆盖反射、序列化和JNI相关的功能路径。多管齐下,一个中型应用从八九十兆降到四五十兆并不困难。