添加保留规则

从广义上讲,保留规则用于指定要保留的类(或子类或实现)以及该类中的成员(方法、构造函数或字段)。

保留规则的一般语法如下,但某些保留选项不接受可选的 keep_option_modfier


-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>

以下是一个保留规则示例,该规则使用 keepclassmembers 作为保留选项,allowoptimization 作为修饰符,并保留 com.example.MyClass 中的 someSpecificMethod()

-keepclassmembers,allowoptimization class com.example.MyClass {
  void someSpecificMethod();
}

保留选项

“保留”选项是保留规则的第一部分。它用于指定要保留类的哪些方面。有六种不同的保留选项,分别是 keepkeepclassmemberskeepclasseswithmemberskeepnameskeepclassmembernameskeepclasseswithmembernames

下表介绍了这些保留选项:

保留选项 Description
keepclassmembers 仅当优化后类存在时,才保留指定的成员
keep 保留指定的类和指定的成员(字段和方法),防止它们被优化。

注意keep 通常应仅与 keep 选项修饰符搭配使用,因为 keep 本身会阻止对匹配的类进行任何类型的优化。
keepclasseswithmembers 仅当类具有类规范中的所有成员时,才保留该类及其指定成员。
keepclassmembernames 防止重命名指定的类成员,但不阻止移除类或其成员。

注意:此选项的含义经常被误解;请考虑改用等效的 -keepclassmembers,allowshrinking
keepnames 防止重命名类及其成员,但如果类及其成员被视为未使用,则不会阻止完全移除它们。

注意:此选项的含义经常被误解;请考虑改用等效的 -keep,allowshrinking
keepclasseswithmembernames 防止重命名类及其指定成员,但前提是这些成员存在于最终代码中。它不会阻止移除代码。

注意:此选项的含义经常被误解;请考虑改用等效的 -keepclasseswithmembers,allowshrinking

选择合适的保留选项

选择合适的保留选项对于确定应用的正确优化至关重要。某些保留选项会缩减代码(即移除未引用的代码),而其他选项则会混淆或重命名代码。下表显示了各种保留选项的操作:

保留选项 缩小类 混淆类 缩小成员 混淆成员
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

保留选项修正值

保留选项修饰符用于控制保留规则的作用域和行为。您可以向保留规则添加 0 个或多个保留选项修饰符。

下表介绍了保留选项修饰符的可能值:

说明
allowoptimization 允许优化指定元素。不过,指定的元素不会被重命名或移除。
allowobfucastion 允许重命名指定元素。不过,这些元素不会被移除或以其他方式进行优化。
allowshrinking 如果 R8 找不到对指定元素的引用,则允许移除这些元素。不过,这些元素不会被重命名或以其他方式优化。
includedescriptorclasses 指示 R8 保留所保留的方法(参数类型和返回类型)和字段(字段类型)的描述符中出现的所有类。
allowaccessmodification 允许 R8 在优化过程中更改(通常是放宽)类、方法和字段的访问修饰符(publicprivateprotected)。
allowrepackage 允许 R8 将类移至不同的软件包,包括默认(根)软件包。

课程规范

您必须在保留规则中指定类、超类或已实现的接口。所有类(包括 java.lang 命名空间中的类,如 java.lang.String)都必须使用其完全限定的 Java 名称进行指定。如需了解应使用的名称,请使用获取生成的 Java 名称中所述的工具检查字节码。

以下示例展示了如何指定 MaterialButton 类:

  • 正确:com.google.android.material.button.MaterialButton
  • 不正确:MaterialButton

类规范还指定了应保留的类中的成员。以下规则会保留 MaterialButton 类及其所有成员

-keep class com.google.android.material.button.MaterialButton { *; }

子类和实现

如需定位到实现接口的子类或类,请分别使用 extendimplements

例如,如果您有类 Bar 和子类 Foo,如下所示:

class Foo : Bar()

以下保留规则会保留 Bar 的所有子类。请注意,keep 规则不包含超类 Bar 本身。

-keep class * extends Bar

如果您有实现 Bar 的类 Foo

class Foo : Bar

以下 keep 规则会保留所有实现 Bar 的类。请注意,keep 规则不包含接口 Bar 本身。

-keep class * implements Bar

访问修饰符

您可以指定 publicprivatestaticfinal 等访问修饰符,使保留规则更加精确。

例如,以下规则会保留 api 软件包及其子软件包中的所有 public 类,以及这些类中的所有 public 和 protected 成员。

-keep public class com.example.api.** { public protected *; }

您还可以为类中的成员使用修饰符。例如,以下规则仅保留 Utils 类的 public static 方法:

-keep class com.example.Utils {
    public static void *(...);
}

Kotlin 特有的修饰符

R8 不支持 Kotlin 特有的修饰符,例如 internalsuspend。请遵循以下准则来保留此类字段。

  • 如需保留 internal 类、方法或字段,请将其视为公开。例如,请考虑以下 Kotlin 源代码:

    package com.example
    internal class ImportantInternalClass {
      internal f: Int
      internal fun m() {}
    }
    

    internal 类、方法和字段在 Kotlin 编译器生成的 .class 文件中是 public,因此您必须使用 public 关键字,如以下示例所示:

    -keepclassmembers public class com.example.ImportantInternalClass {
      public int f;
      public void m();
    }
    
  • 编译 suspend 成员时,请在 keep 规则中匹配其编译后的签名。

    例如,如果您有如下代码段中所示的 fetchUser 函数定义:

    suspend fun fetchUser(id: String): User
    

    编译后,其在字节码中的签名如下所示:

    public final Object fetchUser(String id, Continuation<? super User> continuation);
    

    如需为此函数编写保留规则,您必须匹配此已编译的签名,或使用 ...

    使用已编译的签名的示例如下:

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(java.lang.String,  kotlin.coroutines.Continuation);
    }
    

    使用 ... 的示例如下所示:

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(...);
    }
    

会员规范

类规范可以选择性地包含要保留的类成员。如果您为某个类指定了一个或多个成员,则相应规则仅适用于这些成员。

例如,如需保留特定课程及其所有成员,请使用以下命令:

-keep class com.myapp.MyClass { *; }

如需仅保留类而不保留其成员,请使用以下代码:

-keep class com.myapp.MyClass

大多数情况下,您需要指定一些成员。例如,以下示例保留了类 MyClass 中的公共字段 text 和公共方法 updateText()

-keep class com.myapp.MyClass {
    public java.lang.String text;
    public void updateText(java.lang.String);
}

如需保留所有公共字段和公共方法,请参阅以下示例:

-keep public class com.example.api.ApiClient {
    public *;
}

方法

在保留规则的成员规范中指定方法的语法如下:

[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);

例如,以下保留规则会保留一个名为 setLabel() 的公共方法,该方法返回 void 并接受 String

-keep class com.example.MyView {
    public void setLabel(java.lang.String);
}

您可以使用 <methods> 作为快捷方式来匹配类中的所有方法,如下所示:

-keep class com.example.MyView {
    <methods>;
}

如需详细了解如何为返回类型和参数类型指定类型,请参阅类型

构造函数

如需指定构造函数,请使用 <init>。在保留规则的成员规范中指定构造函数的语法如下:

[<access_modifier>] <init>(parameter_types);

例如,以下 keep 规则会保留采用 ContextAttributeSet 的自定义 View 构造函数。

-keep class com.example.ui.MyCustomView {
    public <init>(android.content.Context, android.util.AttributeSet);
}

如需保留所有公共构造函数,请参考以下示例:

-keep class com.example.ui.MyCustomView {
    public <init>(...);
}

字段

在保留规则的成员规范中指定字段的语法如下:

[<access_modifier>...] [<type>] <field_name>;

例如,以下 keep 规则会保留名为 userId 的私有字符串字段和名为 STATUS_ACTIVE 的公共静态整数字段:

-keep class com.example.models.User {
    private java.lang.String userId;
    public static int STATUS_ACTIVE;
}

您可以使用 <fields> 作为快捷方式来匹配类中的所有字段,如下所示:

-keep class com.example.models.User {
    <fields>;
}

软件包级函数

如需引用在类外部定义的 Kotlin 函数(通常称为顶级函数),请务必使用 Kotlin 编译器隐式添加的类的生成的 Java 名称。类名称是 Kotlin 文件名,后面附加了 Kt。例如,如果您有一个名为 MyClass.kt 的 Kotlin 文件,其定义如下:

package com.example.myapp.utils

// A top-level function not inside a class
fun isEmailValid(email: String): Boolean {
    return email.contains("@")
}

如需为 isEmailValid 函数编写保留规则,类规范需要以生成的类 MyClassKt 为目标:

-keep class com.example.myapp.utils.MyClassKt {
    public static boolean isEmailValid(java.lang.String);
}

类型

本部分介绍如何在 keep 规则成员规范中指定返回类型、参数类型和字段类型。请务必使用生成的 Java 名称来指定类型(如果它们与 Kotlin 源代码不同)。

原始类型

如需指定基本类型,请使用其 Java 关键字。R8 可识别以下基本类型:booleanbyteshortcharintlongfloatdouble

以下是包含原始类型的规则示例:

# Keeps a method that takes an int and a float as parameters.
-keepclassmembers class com.example.Calculator {
    public void setValues(int, float);
}

泛型类型

在编译期间,Kotlin/Java 编译器会擦除泛型类型信息,因此当您编写涉及泛型类型的 keep 规则时,必须以代码的编译表示形式为目标,而不是原始源代码。如需详细了解泛型类型的更改方式,请参阅类型擦除

例如,如果您有以下代码,其中在 Box.kt 中定义了无界泛型类型:

package com.myapp.data

class Box<T>(val item: T) {
    fun getItem(): T {
        return item
    }
}

在类型擦除后,T 会被替换为 Object。为了保留类构造函数和方法,您的规则必须使用 java.lang.Object 代替泛型 T

保留规则的一个示例如下:

# Keep the constructor and methods of the Box class.
-keep class com.myapp.data.Box {
    public init(java.lang.Object);
    public java.lang.Object getItem();
}

如果您在 NumberBox.kt 中有以下代码,其中包含有界限的泛型类型:

package com.myapp.data

// T is constrained to be a subtype of Number
class NumberBox<T : Number>(val number: T)

在这种情况下,类型擦除会将 T 替换为其界限 java.lang.Number

保留规则的一个示例如下:

-keep class com.myapp.data.NumberBox {
    public init(java.lang.Number);
}

如果使用特定于应用的泛型类型作为基类,则必须包含基类的保留规则。

例如,对于以下代码:

package com.myapp.data

data class UnpackOptions(val useHighPriority: Boolean)

// The generic Box class with UnpackOptions as the bounded type
class Box<T: UnpackOptions>(val item: T) {
}

您可以使用包含 includedescriptorclasses 的保留规则,通过单个规则同时保留 UnpackOptions 类和 Box 类方法,如下所示:

-keep,includedescriptorclasses class com.myapp.data.Box {
    public <init>(com.myapp.data.UnpackOptions);
}

如需保留处理对象列表的特定函数,您需要编写一条与该函数的签名完全匹配的规则。请注意,由于泛型类型会被擦除,因此 List<Product> 等参数会被视为 java.util.List

例如,如果您有一个实用程序类,其中包含一个用于处理 Product 对象列表的函数,如下所示:

package com.myapp.utils

import com.myapp.data.Product
import android.util.Log

class DataProcessor {
    // This is the function we want to keep
    fun processProducts(products: List<Product>) {
        Log.d("DataProcessor", "Processing ${products.size} products.")
        // Business logic ...
    }
}

// The data class used in the list (from the previous example)
package com.myapp.data
data class Product(val id: String, val name: String)

您可以使用以下保留规则仅保护 processProducts 函数:

-keep class com.myapp.utils.DataProcessor {
    public void processProducts(java.util.List);
}

数组类型

通过在数组的每个维度的组件类型后附加 [] 来指定数组类型。这适用于类类型和基元类型。

  • 一维类数组:java.lang.String[]
  • 二维原始数组:int[][]

例如,如果您有以下代码:

package com.example.data

class ImageProcessor {
  fun process(): ByteArray {
    // process image to return a byte array
  }
}

您可以使用以下保留规则:

# Keeps a method that returns a byte array.
-keepclassmembers class com.example.data.ImageProcessor {
    public byte[] process();
}

通配符

下表展示了如何使用通配符将保留规则应用于与特定模式匹配的多个类或成员。

通配符 适用于课程或会员 说明
** 二者都有 最常用的。匹配任何类型名称,包括任意数量的软件包分隔符。这对于匹配软件包及其子软件包中的所有类非常有用。
* 二者都有 对于类规范,匹配不包含软件包分隔符 (.) 的类型名称的任何部分。
对于成员规范,匹配任何方法或字段名称。单独使用时,它也是 ** 的别名。
二者都有 匹配类名或成员名称中的任何单个字符。
*** 成员 匹配任何类型,包括基元类型(如 int)、类类型(如 java.lang.String)和任意维度的数组类型(如 byte[][])。
成员 匹配方法的任何参数列表。
% 成员 匹配任何基元类型(例如“int”“float”“boolean”或其他类型)。

以下是一些有关如何使用特殊通配符的示例:

  • 如果您有多个同名方法,但这些方法接受不同的原始类型作为输入,则可以使用 % 编写保留规则来保留所有这些方法。例如,以下 DataStore 类具有多个 setValue 方法:

    class DataStore {
        fun setValue(key: String, value: Int) { ... }
        fun setValue(key: String, value: Boolean) { ... }
        fun setValue(key: String, value: Float) { ... }
    }
    

    以下保留规则会保留所有方法:

    -keep class com.example.DataStore {
        public void setValue(java.lang.String, %);
    }
    
  • 如果您有多个类,它们的名称仅相差一个字符,请使用 ? 编写保留规则,以保留所有这些类。例如,如果您有以下类:

    com.example.models.UserV1 {...}
    com.example.models.UserV2 {...}
    com.example.models.UserV3 {...}
    

    以下 keep 规则会保留所有类:

    -keep class com.example.models.UserV?
    
  • 如需匹配 ExampleAnotherExample 类(如果它们是根级类),但不匹配 com.foo.Example 类,请使用以下 keep 规则:

    -keep class *Example
    
  • 如果您单独使用 *,它会充当 ** 的别名。例如,以下保留规则是等效的:

    -keepclasseswithmembers class * { public static void main(java.lang.String[];) }
    
    -keepclasseswithmembers class ** { public static void main(java.lang.String[];) }
    

检查生成的 Java 名称

编写保留规则时,您必须使用类和其他引用类型在编译为 Java 字节码后的名称来指定它们(有关示例,请参阅类规范类型)。如需查看代码生成的 Java 名称,请使用 Android Studio 中的以下任一工具:

  • APK 分析器
  • 打开 Kotlin 源文件后,依次前往 Tools > Kotlin > Show Kotlin Bytecode > Decompile,检查字节码。