引言
你是否也到过自己的Android项目,在debug
时项目是正常运行的,但在release
时,项目却出现了找不到对应的类的问题?
导致这个问题的原因有很多,其中最最主要原因是debug
模式为了方便调试,默认关闭代码压缩、混淆、资源优化;而 release
模式为了减小包体积、提升性能,会启用代码压缩、混淆、资源压缩 ,这些优化操作若配置不当,会直接导致类被 "误删除" 或 "混淆后找不到"。其中,代码压缩、混淆、资源压缩 这些工作均由 R8 来实现。
什么是 R8
R8 是 Google 推出的 Android 代码压缩和混淆工具,自 AGP 3.4.0 起默认的代码优化工具。它在构建 release 版本时执行以下核心功能:
- 代码混淆(Obfuscation) :将类名、方法名、字段名等有意义的标识符重命名为极短的无意义名称(如
a
,b
),增加反向工程难度。 - 代码压缩(Shrinking) :分析代码的可达性,移除项目中未被使用的类、方法、字段等,减小 APK 体积。
- 代码优化(Optimization) :进行内联、移除无用代码块、简化控制流等操作,提升应用运行效率。
R8 直接将 Java 字节码(.class
文件)转换为优化后的 DEX
字节码,通常具有更快的构建速度和更好的优化效果。
使用与关闭 R8 相关功能
R8 相关功能的启用与关闭可以在 build.gradle
中进行配置
kotlin
android {
buildTypes {
release {
minifyEnabled true // 启用 R8 代码压缩和混淆
shrinkResources true // 启用资源压缩
// 下面是R8规则的配置文件
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
R8 原理与相关功能介绍
混淆代码
混淆代码(Obfuscation)是指将源代码中的类名、方法名、字段名等 有意义的标识符替换为无意义的短名称(如 a
, b
, c
等),从而增加逆向工程的难度,保护应用逻辑不被轻易窃取。
作用:
- 提高安全性:防止他人通过反编译轻松理解你的代码逻辑。
- 减小 DEX 文件体积:较短的名称占用更少空间。
- 避免暴露敏感信息:例如内部 API 名称、业务逻辑命名等。
当 minifyEnabled true
启用后,R8 会自动执行混淆操作。默认情况下,R8 使用启发式算法分析代码入口点(如 Activity
、Application
类等),仅对未被引用或可安全处理的部分进行重命名。
配置方式
Android Studio在创建项目时会创建proguard-rules.pro
文件,可以在这个文件中添加混淆规则:
ini
-keep class com.example.UserManager { *; }
-keepclassmembers class * implements java.io.Serializable {
<fields>;
<methods>;
}
压缩代码与资源
1. 代码压缩
定义:代码压缩是指:移除应用中未被使用的类、方法、字段和指令,只保留运行时真正需要的代码。
工作原理 : R8 从入口点(如 Android 四大组件、自定义 Application
类等)开始追踪所有可达代码路径。任何无法从入口点访问到的代码都会被视为"死代码"并被删除 (可达性分析)
启用方式: 同代码混淆
2. 资源压缩(Resource Shrinking)
定义 :
资源压缩是指移除打包进 APK 中但实际未被代码引用的资源文件(如图片、布局 XML、字符串等)。
工作机制:
- R8 分析代码中对资源的引用(包括
R.*
和Resources.getIdentifier()
)。 - 构建一个"引用图",标记所有被使用的资源。
- 未出现在图中的资源将在打包时被剔除。
启用方式:
kotlin
shrinkResources true // 通常配合 minifyEnabled 使用
代码优化
定义
对字节码进行深层次的结构优化,在不改变程序行为的前提下提升性能和效率。
典型优化策略
优化类型 | 说明 |
---|---|
内联方法(Method Inlining) | 将小方法体直接插入调用处,减少方法调用开销。 |
常量传播(Constant Propagation) | 将常量值传播到使用位置,消除变量访问。 |
无用代码消除(Dead Code Elimination) | 移除永远无法执行的代码块(如 if (false) )。 |
类合并与简化 | 合并仅被单处使用的类,或简化继承结构。 |
字符串优化 | 合并重复字符串,移除冗余拼接。 |
R8 的优化是在字节码层面完成的,开发者无需手动干预。
总结对比
功能 | 是否默认启用 | 主要收益 |
---|---|---|
混淆 | minifyEnabled true 时启用 |
安全性、体积减小 |
代码压缩 | minifyEnabled true |
移除无用代码 |
资源压缩 | shrinkResources true |
移除无用资源 |
优化 | minifyEnabled true 时自动启用 |
性能提升、体积进一步压缩 |
注意事项
那些类不应该被 R8 处理
为了确保应用正常运行,一些类、方法或字段必须保留原样,不可被 R8 压缩或混淆:
反射相关的类和成员
使用 Java 反射(Class.forName()
, getDeclaredMethod()
等)调用的类、方法、字段必须保留原名。
kotlin
# 保留特定类及其所有成员不被混淆
-keep class com.example.MyReflectionTarget { *; }
# 或者更精确地保留特定方法
-keepclassmembers class com.example.MyClass {
public void reflectiveMethod();
}
JNI 方法
所有通过 native
关键字声明并在 C/C++ 代码中实现的方法。
kotlin
# 保留 native 方法的签名
-keepclasseswithmembers class * {
native <methods>;
}
# 或者针对特定类
-keepclassmembers class com.example.NativeBridge {
public native *;
}
序列化
Serializable
: 实现java.io.Serializable
的类,其字段名在反序列化时需匹配。Parcelable
: 通常由框架生成代码处理,一般无需额外规则,但复杂情况可能需要。JSON 序列化库 (Gson, Moshi, Jackson)
: 需要保留模型类的字段名和构造函数。
kotlin
# 保留所有实现 Serializable 的类
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# Gson: 保留所有被注解的成员和无参构造函数
-keepattributes Signature
-keepattributes *Annotation*
-keepclassmembers class ** {
@com.google.gson.annotations.* <fields>;
}
-keepclassmembers class * {
*** get*();
*** is*();
*** set*(***);
}
四大组件
Android 系统通过类名反射创建这些组件,因此它们的类名必须保留。
kotlin
# 通常 AndroidX 或 Support Library 的规则已包含
-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
-keep public class * extends android.app.Application
自定义 View
在布局 XML 中引用的自定义 View 类及其构造函数。
kotlin
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
第三方库
许多第三方库都有特定的 ProGuard 规则。要查阅官方文档并添加相应规则。
使用注解处理器生成代码的类
Room
会根据注解自动生成代码,它们生成的代码依赖于原始类的结构和注解,通常需要保留原始类,涉及到Room的类应当被保留。
公共 API
如果应用是库模块,需要明确保留对外暴露的 API。
kotlin
-keep public class com.example.mylibrary.PublicApi { *; }