一、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.xmlic_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做的事情有三件:

  1. 压缩(Shrink) — 删除未使用的代码
  2. 优化(Optimize) — 简化代码逻辑
  3. 混淆(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分析,看看体积变化趋势。如果某个版本体积突然增加了,那肯定是有东西被误加进来了。及时发现问题,比事后补救要轻松得多。

APK包体积优化核心路径 APK包体积优化 资源混淆 代码混淆 动态库裁剪 ABI分包 AndResGuard ProGuard / R8 strip / abiFilters App Bundle / Splits 综合效果 APK体积减少 30% ~ 60%
公众号:蓝海资料掘金营,微信deep3321