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左右,效果很明显。

小技巧:资源混淆后,如果某些资源被第三方SDK通过反射引用,可能会崩溃。记得把第三方SDK的R类加入白名单。我一般会先跑一次混淆,然后看崩溃日志,逐步补充白名单。

动态库按需加载

动态库(.so文件)是包体积的大头。一个完整的FFmpeg库动辄十几MB,如果全量打包进APK,用户下载时就得等半天。我做过一个视频编辑App,最开始把armeabi-v7a、arm64-v8a、x86、x86_64四个架构的so库全打进去了,APK直接飙到50MB。

按需加载的核心思路是:只打包当前设备需要的架构。具体做法有两种:

  1. APK分包:在 build.gradle 里配置abiFilters,只保留主流架构:
android {
    defaultConfig {
        ndk {
            abiFilters "arm64-v8a", "armeabi-v7a"
        }
    }
}

这样打包出来的APK只包含arm架构的so库,x86设备虽然不能直接安装,但Google Play会通过APK Split自动分发对应架构的版本。如果你在国内市场分发,可以单独打一个x86包。

  1. 动态下载:把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());
    }
}
注意:动态下载so库会延长App的启动时间。我建议在App首次启动时下载核心so库,非核心功能可以延迟加载。另外,记得处理下载失败的回退逻辑——如果下载失败,可以提示用户重试,或者走降级方案。

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_nameR.string.app_title 如果值相同,R8会合并成一个。这个功能默认是开启的,不需要额外配置。

个人经验:我建议在debug模式下也开启R8,但只做混淆不做优化。这样可以在开发阶段就发现混淆导致的问题,避免拖到发版前才手忙脚乱。配置如下:
debug {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

知识体系总览

下面这张图是我整理的包体积优化知识体系,涵盖了资源、动态库、代码三个维度。你可以对照着检查自己的项目:

包体积优化知识体系 资源混淆与去冗余 动态库按需加载 R8/ProGuard配置 shrinkResources + Lint 清理 AndResGuard 资源混淆 白名单管理(反射/第三方) abiFilters 按架构分包 动态下载 + MD5校验 延迟加载 + 降级方案 精准keep规则(反射/JNI) Debug模式开启混淆 资源字符串合并优化 目标:APK体积减少30%~50%,同时保证功能完整

最后说一句,包体积优化不是一锤子买卖。我建议每次发版前都跑一遍优化流程,把新增的资源、代码、so库都检查一遍。久而久之,你就会形成习惯,看到APK体积异常增长时,能快速定位到问题。

嗯,今天就聊到这儿。如果你在配置R8时遇到奇怪的崩溃,或者资源混淆后某个页面显示异常,欢迎随时交流。这些坑我都踩过,能帮你省不少时间。

公众号:蓝海资料掘金营,微信deep3321