混淆与签名打包
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 给国内市场。
常见坑与最佳实践
- 混淆后 Gson 解析返回 null:POJO 类没加
@Keep或proguard-rules.pro没 keep,字段名被改成a、b,JSON 的name字段匹配不上。给所有 model 包加-keep class com.example.app.model.** { *; }。 - release 包崩溃 debug 不崩:99% 是混淆删了反射调用的类。检查
mapping.txt反混淆崩溃堆栈定位,再补 keep 规则。 - keystore 丢失无法更新:没有备份或员工离职带走。启用 Play App Signing 让 Google 托管上传密钥;本地 keystore 仅作“上传密钥”,丢了可申请重置。
- debug 与 release 行为不一致:debug 没开混淆,release 把 Log 代码删了或把单例混淆出问题。在 release 用
BuildConfig.DEBUG区分,必要时proguard-rules.pro加-assumeno side effects移除Log.d/Log.v。 - shrinkResources 误删动态资源:
getIdentifier("icon_" + id, "drawable", ...)取的资源被删,运行时返回 0。在res/raw/keep.xml用tools:keep声明。 - 多渠道 applicationId 冲突:不同 flavor 用了相同
applicationId,安装时互相覆盖。每个 flavor 用applicationIdSuffix或独立applicationId。 - V1 签名未开导致 7.0 以下安装失败:只开了 V2,Android 6.0 设备无法识别签名。AGP 默认会同时开 V1+V2,但手动配置时别关 V1。
- mapping.txt 没归档:线上崩溃堆栈是
a.b.c(),没有对应版本 mapping 无法还原。每次发布把 mapping.txt 上传到 Crashlytics(见第 40 章)或归档到内部系统。 - password 明文提交到 git:泄露密钥的风险。用环境变量/
~/.gradle/gradle.properties,CI 用 Secrets。 - 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 上传、商店列表(截图/描述/分类)、内容分级问卷、隐私政策,以及从内测轨道到正式发布的分阶段发布策略。