A
第 37 章JAVA40 分钟

混淆与签名打包

Android 混淆与签名打包:R8/ProGuard 工作原理、keep 规则(@Keep/keep.xml)、签名体系(V1/V2/V3/V4)、BuildVariant、ProductFlavor 多渠道、APK 与 AAB 构建产物解析。

学习目标

  • 理解 R8/ProGuard 混淆与压缩的工作原理
  • 能够编写 keep 规则保留反射调用的类与成员
  • 掌握 keystore 生成与 V1/V2/V3 签名方案
  • 会用 BuildVariant 与 ProductFlavor 配置 debug/release 与多渠道
  • 理解 APK 与 AAB 两种构建产物的差异与适用场景

学习目标

  • 理解 R8/ProGuard 混淆与压缩的工作原理;
  • 能够编写 keep 规则保留反射调用的类与成员;
  • 掌握 keystore 生成与 V1/V2/V3 签名方案;
  • 会用 BuildVariant 与 ProductFlavor 配置 debug/release 与多渠道;
  • 理解 APK 与 AAB 两种构建产物的差异与适用场景。

混淆与签名打包

应用开发完成、测试通过后,下一步就是把代码“瘦身 + 防读”并打上签名,准备上架。本章覆盖从源码到产物的完整链路:混淆(R8)→ keep 规则 → 签名 → BuildVariant → 多渠道 → APK/AAB。

一、R8 与 ProGuard

1. 它们是什么

  • ProGuard:老牌 Java 字节码优化器,做四件事——压缩(Shrinking,移除未使用代码)、优化(Optimization,方法内联等)、混淆(Obfuscation,把类/方法/字段名改成 a、b、c)、预校验(Preverification);
  • R8:Google 在 Android Gradle Plugin 3.4 起默认替代 ProGuard 的工具,兼容 ProGuard keep 规则语法,并额外做 desugaring(脱糖,让低版本用 Java 8+ API)和 D8 dexing(编译成 dex)。从 AGP 8.0 起,R8 是默认且唯一的混淆器。

R8 的四项工作:

源码 .class ──┐
              ├─→ 压缩 → 优化 → 混淆 → desugar → dex → 输出 APK/AAB
资源 .xml   ──┘

2. 启用混淆

在 build.gradle(注意:本系列 Shiki 不支持 gradle 语言标记,下面用 bash 代替)中开启 minifyEnabled:

android {
    compileSdk 34

    buildTypes {
        release {
            minifyEnabled true          // 开启 R8
            shrinkResources true        // 移除未引用资源
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
}
  • minifyEnabled true:开启 R8(包含混淆、压缩、优化);
  • shrinkResources true:移除未引用的图片/布局等资源(必须先开 minifyEnabled);
  • proguard-android-optimize.txt:Google 提供的默认规则,启用优化;
  • proguard-rules.pro:项目自定义规则。

3. keep 规则:保留不能被混淆的代码

R8 默认会删除“看起来没人调用”的代码、把名字改短。但下面这些场景不能删/不能改名:

  • 反射调用的类(Gson 解析的 POJO、Class.forName("..."));
  • JNI 调用的 native 方法;
  • JSInterface 注入 WebView 的方法;
  • 序列化字段(被 JSON/数据库按字段名读取)。

保留方式有三种。

方式一:proguard-rules.pro 文件

# 保留所有 Activity(清单文件声明,被系统反射启动)
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.app.Application

# 保留 Gson 模型的字段(反射读取字段名)
-keep class com.example.app.model.** { *; }

# 保留 native 方法
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留枚举的 values/valueOf(反射常用)
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留带 @Keep 注解的类/成员
-keep,allowobfuscation @interface androidx.annotation.Keep
-keep @androidx.annotation.Keep class *
-keepclassmembers @androidx.annotation.Keep class * {
    *;
}

常用指令速查:

指令 作用
-keep class ** 保留类名(不删除、不混淆)
-keepclassmembers 保留成员(类本身按默认处理)
-keepnames 仅保留名字(防止混淆),不防止删除
-dontwarn 忽略未找到类的警告
-dontoptimize 关闭优化
-dontobfuscate 关闭混淆(仅做压缩)

方式二:@Keep 注解(推荐)

AndroidX 提供 androidx.annotation.Keep,注解到类/方法/字段上,配合默认规则即可保留:

import androidx.annotation.Keep;

@Keep
public class UserResponse {
    public String name;
    public int age;

    @Keep
    public UserResponse() {}  // Gson 反射需要无参构造

    @Keep
    public void onJsCallback(String data) { /* JSInterface */ }
}

注解的好处是和代码强绑定,改名/移动时不会丢失规则。注意:默认规则文件 proguard-android-optimize.txt 已经声明了 -keep @Keep class * 等规则,但有时自定义项目需手动加(见上面 proguard-rules.pro 示例)。

方式三:keep.xml 资源级保留

shrinkResources true 会移除“代码中未引用”的资源。但动态加载(getIdentifier 按名字取资源)的资源 R8 看不出来,会误删。在 res/raw/keep.xml 声明:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@drawable/icon_dynamic,@layout/overlay_*"
    tools:discard="@layout/debug_unused" />

构建后会在 build/outputs/mapping/release/ 看到这些文件:

  • mapping.txt:原始名 → 混淆名的映射,必须保留用于反混淆崩溃堆栈;
  • seeds.txt:被 keep 规则匹配的类/成员;
  • usage.txt:被删除的代码;
  • configuration.txt:最终生效的完整规则。

上架后用户崩溃堆栈里全是 a.b.c(),没有 mapping.txt 无法定位。建议每次发布把 mapping.txt 与版本号一起归档。

4. 常见库的 keep 规则

主流库通常已自带 consumer-rules.pro(依赖会自动合并),但偶有遗漏需手动:

# Retrofit:保留注解和接口方法签名
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
-keepclassmembers,allowshrinking,allowobfuscation interface * {
    @retrofit2.http.* <methods>;
}

# OkHttp 3 / 4
-dontwarn okhttp3.**
-dontwarn okio.**

# Gson
-keep class com.google.gson.** { *; }
-keep class sun.misc.Unsafe { *; }
-keep @com.google.gson.annotations.SerializedName class * { *; }

# Room
-keep class * extends androidx.room.RoomDatabase { *; }

二、签名体系

Android 要求所有 APK/AAB 必须签名才能安装。签名机制经过几代演进:

方案 引入版本 特点
V1(jarsigner) Android 1.0 基于 JAR 签名,按文件逐个校验,速度慢,支持 META-INF 内修改
V2(APK Signature Scheme v2) Android 7.0 (API 24) 整包签名,覆盖 ZIP 全部内容,校验快、防篡改强
V3 Android 9.0 (API 28) 支持密钥轮换(key rotation),签名里带历史密钥链
V4 Android 11 (API 30) 增量安装签名,配合 adb install --incremental 加速大 APK 安装

新方案向后兼容:建议同时打 V1+V2+V3,让旧设备也能装。

1. 生成 keystore

用 keytool(JDK 自带)生成签名密钥库:

keytool -genkeypair -v ^
  -keystore release.keystore ^
  -alias release-key ^
  -keyalg RSA -keysize 2048 -validity 10000 ^
  -storepass 123456 -keypass 123456 ^
  -dname "CN=MyApp, OU=Dev, O=MyCompany, L=Beijing, ST=Beijing, C=CN"

参数说明:

  • -keystore:密钥库文件路径,release.keystore;
  • -alias:别名,一个 keystore 可存多个 key,用 alias 区分;
  • -keyalg RSA -keysize 2048:RSA 2048 位,足够安全;
  • -validity 10000:有效期 10000 天(约 27 年),必须 ≥ 25 年(Google Play 要求到 2033 年之后);
  • -dname:Distinguished Name,CN=名字,O=组织。

强烈建议把 keystore 与密码备份到公司保险柜/密码管理器,丢失后无法更新应用(Google Play 现支持 Play App Signing 由 Google 托管上传密钥,可缓解,但首次配置仍需谨慎)。

2. 配置签名

把 keystore 放到项目根 app/release.keystore,在 build.gradle 配置:

android {
    signingConfigs {
        release {
            storeFile file('release.keystore')
            storePassword '123456'
            keyAlias 'release-key'
            keyPassword '123456'
            // 启用 V1/V2/V3 签名(AGP 4.2+ 默认全开)
            v1SigningEnabled true
            v2SigningEnabled true
            v3SigningEnabled true
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            // ...
        }
    }
}

密码不要硬编码到 git。推荐做法:

  • 把密码放 ~/.gradle/gradle.properties(不进版本控制):
RELEASE_STORE_PASSWORD=123456
RELEASE_KEY_PASSWORD=123456
  • build.gradle 读环境变量:
release {
    storeFile file('release.keystore')
    storePassword System.getenv('RELEASE_STORE_PASSWORD') ?: ''
    keyAlias 'release-key'
    keyPassword System.getenv('RELEASE_KEY_PASSWORD') ?: ''
}

CI 环境(GitHub Actions / GitLab CI)用 Secrets 注入环境变量。

3. 签名校验

构建完后用 apksigner 验证签名方案:

apksigner verify --verbose --print-certs app-release.apk

输出会显示 Verified using v1 scheme (JAR signing)、Verified using v2 scheme (APK Signature Scheme v2) 等。

三、BuildVariant:debug 与 release

Gradle 默认有两个 BuildType:debug 与 release。它们和 ProductFlavor 组合,形成最终的 BuildVariant(变体)。

1. BuildType

android {
    buildTypes {
        debug {
            applicationIdSuffix '.debug'        // 包名加后缀,可与正式版共存
            versionNameSuffix '-DEBUG'
            minifyEnabled false
            debuggable true
        }
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                    'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
}

applicationIdSuffix '.debug' 让 debug 包名变成 com.example.app.debug,可以和正式版同时安装(开发期间常用)。

2. ProductFlavor:多渠道

每个 flavor 可以定义独立的 applicationId、版本号、资源、代码。典型场景:免费版/付费版、国内/海外。

android {
    flavorDimensions 'channel'      // 必须声明 dimension

    productFlavors {
        google {
            dimension 'channel'
            applicationId 'com.example.app'
            buildConfigField 'String', 'MARKET', '"google"'
            resValue 'string', 'app_name', 'MyApp'
        }
        huawei {
            dimension 'channel'
            applicationId 'com.example.app.huawei'
            buildConfigField 'String', 'MARKET', '"huawei"'
            resValue 'string', 'app_name', 'MyApp (华为)'
        }
    }
}
  • flavorDimensions:维度,多个 flavor 必须归类到 dimension(可多个维度交叉,例如 channel × env);
  • buildConfigField:注入 BuildConfig.MARKET 常量,运行时读取;
  • resValue:注入字符串资源,可在 XML 中 @string/app_name 引用。

每个 flavor 还可放独立源码集 src/google/java/、src/huawei/res/,Gradle 会自动合并。

代码里读渠道:

public class MarketUtil {
    public static String currentMarket() {
        return BuildConfig.MARKET;  // "google" 或 "huawei"
    }
}

3. 变体组合

flavor × buildType 形成变体,每个变体独立构建:

google + debug  → googleDebug
google + release → googleRelease
huawei + debug  → huaweiDebug
huawei + release → huaweiRelease

在 Android Studio 的 Build Variants 面板切换要构建的变体,命令行用 ./gradlew assembleGoogleRelease 等。

四、构建产物:APK 与 AAB

1. APK

直接安装包,所有屏幕密度/CPU 架构的资源和 so 都打进去:

./gradlew assembleRelease
# 输出:app/build/outputs/apk/release/app-release.apk

体积大,但可以任意分发(官网、第三方市场、企业内网)。

2. AAB(Android App Bundle)

Google Play 自 2021 年起强制要求新应用用 AAB 上传。AAB 是发布格式,不是安装格式:

./gradlew bundleRelease
# 输出:app/build/outputs/bundle/release/app-release.aab

上传到 Play 后,Play 服务器根据用户设备的屏幕密度/CPU/语言动态拆分成单 APK(Dynamic Delivery),用户下载的只是自己需要的那部分,体积可减少 20%-60%。

3. 本地从 AAB 生成 APK 测试

AAB 不能直接安装,需要用 bundletool 转成 APK:

# 生成全设备 APK(用于本地测试)
java -jar bundletool.jar build-apks --bundle=app-release.aab --output=app.apks

# 生成指定设备 APK 并安装
java -jar bundletool.jar build-apks --bundle=app-release.aab --output=app.apks ^
    --device-spec=device.json
java -jar bundletool.jar install-apks --apks=app.apks

app.apks 是一个 zip,里面含多个 split APK。bundletool 在 Google 的 bundletool 仓库 下载。

4. APK 与 AAB 对比

维度 APK AAB
性质 安装格式 发布格式
体积 大(全 ABI/密度) 上传小,下发更小(按设备拆分)
安装 直接 adb install 需 bundletool 转 APK
分发 任意渠道 仅 Google Play(国内市场大多接受 APK)
Dynamic Feature 不支持 支持(按需下载模块)
签名 用你的 keystore 签 Play App Signing 由 Google 签最终包

国内发布(华为/小米/OPPO/vivo/应用宝)通常仍接受 APK;同时上架国内外的话,AAB 给 Google Play,APK 给国内市场。

常见坑与最佳实践

  1. 混淆后 Gson 解析返回 null:POJO 类没加 @Keep 或 proguard-rules.pro 没 keep,字段名被改成 a、b,JSON 的 name 字段匹配不上。给所有 model 包加 -keep class com.example.app.model.** { *; }。
  2. release 包崩溃 debug 不崩:99% 是混淆删了反射调用的类。检查 mapping.txt 反混淆崩溃堆栈定位,再补 keep 规则。
  3. keystore 丢失无法更新:没有备份或员工离职带走。启用 Play App Signing 让 Google 托管上传密钥;本地 keystore 仅作“上传密钥”,丢了可申请重置。
  4. debug 与 release 行为不一致:debug 没开混淆,release 把 Log 代码删了或把单例混淆出问题。在 release 用 BuildConfig.DEBUG 区分,必要时 proguard-rules.pro 加 -assumeno side effects 移除 Log.d/Log.v。
  5. shrinkResources 误删动态资源:getIdentifier("icon_" + id, "drawable", ...) 取的资源被删,运行时返回 0。在 res/raw/keep.xml 用 tools:keep 声明。
  6. 多渠道 applicationId 冲突:不同 flavor 用了相同 applicationId,安装时互相覆盖。每个 flavor 用 applicationIdSuffix 或独立 applicationId。
  7. V1 签名未开导致 7.0 以下安装失败:只开了 V2,Android 6.0 设备无法识别签名。AGP 默认会同时开 V1+V2,但手动配置时别关 V1。
  8. mapping.txt 没归档:线上崩溃堆栈是 a.b.c(),没有对应版本 mapping 无法还原。每次发布把 mapping.txt 上传到 Crashlytics(见第 40 章)或归档到内部系统。
  9. password 明文提交到 git:泄露密钥的风险。用环境变量/~/.gradle/gradle.properties,CI 用 Secrets。
  10. AAB 上传后被拒:Play 对 AAB 有额外要求(如 base 模块必须含启动 Activity)。本地先用 bundletool 转 APK 跑一遍安装测试再上传。

章节小结

本章覆盖了从源码到产物的完整链路:

  • R8:默认混淆器,做压缩、优化、混淆、desugaring;用 minifyEnabled true + shrinkResources true 开启;
  • keep 规则:通过 proguard-rules.pro、@Keep 注解、keep.xml 三种方式保留反射调用的类/成员/资源;发布后必须归档 mapping.txt;
  • 签名:V1(jarsigner)→ V2(整包)→ V3(密钥轮换)→ V4(增量安装),用 keytool 生成 keystore,密码走环境变量;
  • BuildVariant:BuildType(debug/release)× ProductFlavor(多渠道)组合出变体,可注入 applicationId/BuildConfig/资源;
  • 产物:APK 用于任意渠道直接安装,AAB 用于 Google Play 动态下发,体积更小。国内+海外同时发布时 AAB 给 Play、APK 给国内市场。

下一章预告

打包完成,下一步是上架 Google Play。第 38 章将走完整个上架流程:开发者账号注册、应用创建、AAB 上传、商店列表(截图/描述/分类)、内容分级问卷、隐私政策,以及从内测轨道到正式发布的分阶段发布策略。