充分发挥 R8 优化器的潜力

R8 提供两种模式:兼容模式和完整模式。完整模式可提供强大的优化功能,以提升应用性能。

本指南适用于想要使用 R8 最强大的优化功能的 Android 开发者。本文探讨了兼容模式和完整模式之间的主要区别,并提供了安全迁移项目和避免常见运行时崩溃所需的明确配置。

启用完整模式

如需启用完整模式,请从 gradle.properties 文件中移除以下行:

android.enableR8.fullMode=false // Remove this line to enable full mode

保留与属性关联的类

属性是存储在已编译类文件中的元数据,不属于可执行代码。不过,在某些类型的反思中,它们可能必不可少。常见示例包括 Signature(在类型擦除后保留泛型类型信息)、InnerClassesEnclosingMethod(用于反映类结构)以及运行时可见的注释。

以下代码展示了字节码中字段的 Signature 属性。对于字段:

List<User> users;

编译后的类文件将包含以下字节码:

.field public static final users:Ljava/util/List;
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "Ljava/util/List<",
            "Lcom/example/package/User;",
            ">;"
        }
    .end annotation
.end field

大量使用反射的库(例如 Gson)通常依赖于这些属性来动态检查和了解代码的结构。在 R8 的完整模式下,默认情况下,只有在明确保留关联的类、字段或方法时,才会保留属性。

以下示例展示了为什么需要属性,以及从兼容模式迁移到完整模式时需要添加哪些保留规则。

请看以下示例,其中我们使用 Gson 库反序列化用户列表。


import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

data class User(
    @SerializedName("username")
    var username: String? = null,
    @SerializedName("age")
    var age: Int = 0
)

fun GsonRemoteJsonListExample() {
    val gson = Gson()

    // 1. The JSON string for a list of users returned from remote
    val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""

    // 2. Deserialize the JSON string into a List<User>
    // We must use TypeToken for generic types like List
    val listType = object : TypeToken<List<User>>() {}.type
    val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)

    // Print the list
    println("First user from list: ${deserializedList}")
}

在编译期间,Java 的类型擦除会移除泛型类型实参。这意味着,在运行时,List<String>List<User> 都会显示为原始 List。因此,依赖反射的 Gson 等库在反序列化 JSON 列表时无法确定 List 声明要包含的特定对象类型,这可能会导致运行时问题。

为了保留类型信息,Gson 使用 TypeToken。封装 TypeToken 会保留必要的反序列化信息。

Kotlin 表达式 object:TypeToken<List<User>>() {}.type 会创建一个扩展 TypeToken 并捕获泛型类型信息的匿名内部类。在此示例中,匿名类的名称为 $GsonRemoteJsonListExample$listType$1

Java 编程语言会将超类的泛型签名作为元数据(称为 Signature 属性)保存在已编译的类文件中。然后,TypeToken 使用此 Signature 元数据在运行时恢复类型。 这样,Gson 就可以使用反射来读取 Signature,并成功发现反序列化所需的完整 List<User> 类型。

在兼容模式下启用 R8 时,即使未明确定义具体的保留规则,它也会保留类的 Signature 属性,包括 $GsonRemoteJsonListExample$listType$1 等匿名内部类。因此,R8 兼容性模式不需要任何进一步的显式保留规则,此示例即可按预期运行。

// keep rule for compatibility mode
-keepattributes Signature

如果 R8 在完整模式下处于启用状态,则会剥离匿名内部类 $GsonRemoteJsonListExample$listType$1Signature 属性。如果 Signature 中没有此类信息,Gson 就无法找到正确的应用类型,从而导致 IllegalStateException。为防止出现这种情况,必须添加以下保留规则:

// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
  • -keepattributes Signature:此规则指示 R8 保留 Gson 需要读取的属性。在完整模式下,R8 仅为 keep 规则明确匹配的类、字段或方法保留 Signature 属性。

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken:此规则是必需的,因为 TypeToken 封装了正在反序列化的对象的类型。在类型擦除后,系统会创建一个匿名内部类来保留泛型类型信息。如果不明确保留 com.google.gson.reflect.TypeToken,则处于完整模式的 R8 不会在反序列化所需的 Signature 属性中包含此类。

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken:此规则会保留扩展 TypeToken 的匿名类的类型信息,例如此示例中的 $GsonRemoteJsonListExample$listType$1。如果没有此规则,R8 在完整模式下会剥离必要的类型信息,导致反序列化失败。

从 Gson 2.11.0 版开始,该库捆绑了在完整模式下进行反序列化所需的必要保留规则。在启用 R8 的情况下构建应用时,R8 会自动从库中查找并应用这些规则。这样一来,您的应用就能获得所需的保护,而您无需在项目中手动添加或维护这些特定规则。

请务必了解,前面分享的规则仅解决了发现泛型类型的问题(例如,List<User>)。R8 还会重命名类的字段。如果您不在数据模型中使用 @SerializedName 注解,Gson 将无法反序列化 JSON,因为字段名称将不再与 JSON 键匹配。

不过,如果您使用的 Gson 版本低于 2.11,或者您的模型未使用 @SerializedName 注释,则必须为这些模型添加明确的保留规则。

保留默认构造函数

在 R8 完全模式下,即使类本身被保留,无实参/默认构造函数也不会被隐式保留。如果您使用 class.getDeclaredConstructor().newInstance()class.newInstance() 创建类的实例,则必须在完整模式下显式保留无实参构造函数。相比之下,兼容模式始终保留无实参构造函数。

以一个示例为例,其中使用反射创建 PrecacheTask 的实例,以动态调用其 run 方法。虽然此场景在兼容模式下不需要额外的规则,但在完整模式下,PrecacheTask 的默认构造函数会被移除。因此,需要制定特定的保留规则。

// In library
interface StartupTask {
    fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
    fun execute(taskClass: Class<out StartupTask>) {
        // The class isn't removed, but its constructor might be.
        val task = taskClass.getDeclaredConstructor().newInstance()
        task.run()
    }
}

// In app
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("Pre cache task", "Warming up the cache...")
    }
}

fun runTaskRunner() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified

-keep class com.example.fullmoder8.PreCacheTask {
    <init>();
}

默认情况下,访问权限修改功能处于启用状态

在兼容模式下,R8 不会更改类中方法和字段的可见性。不过,在完整模式下,R8 会通过更改方法和字段的可见性(例如从私有更改为公开)来增强优化效果。 这样可以实现更多内嵌。

如果您的代码使用的反射专门依赖于具有特定可见性的成员,则此优化可能会导致问题。R8 不会识别这种间接使用情况,可能会导致应用崩溃。为防止出现这种情况,您必须添加特定的 -keep 规则来保留成员,这也会保留其原始可见性。

如需了解详情,请参阅此示例,了解为什么不建议使用反射来访问私有成员,以及保留这些字段/方法的 keep 规则。