以下示例基于您使用 R8 进行优化的常见场景,但需要高级指南来编写保留规则。
反射
一般来说,为了获得最佳性能,不建议使用反射。 不过,在某些情况下,反射可能是不可避免的。以下示例针对使用反射的常见场景提供了保留规则指南。
如果反射位于您自己的应用或库代码中,则可以使用 keep annotations,而无需手动编写保留规则。Gradle 插件会根据源代码中的注解生成保留规则,包括库的消费者保留规则。
通过名称加载类的反射
库通常使用类名称作为 String 动态加载类。
不过,R8 无法检测以这种方式加载的类,并且可能会移除它认为未使用的类。
例如,考虑以下场景:您有一个库和一个使用该库的应用 - 该代码演示了一个库加载器,该加载器实例化了由应用实现的 StartupTask 接口。
库代码如下所示:
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(className: String) {
// R8 won't retain classes specified by this string value at runtime
val taskClass = Class.forName(className)
val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
task.run()
}
}
使用该库的应用具有以下代码:
// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
override fun run() {
// This log will never appear if the class is removed by R8.
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is told to run the app's task by its name.
TaskRunner.execute("com.example.app.PreCacheTask")
}
在这种情况下,您的库应包含一个消费者保留规则文件,其中包含以下保留规则:
-keep class * implements com.example.library.StartupTask {
<init>();
}
如果没有此规则,R8 会从应用中移除 PreCacheTask,因为应用不会直接使用该类,从而破坏集成。该规则会查找实现库的 StartupTask 接口的类并保留这些类及其无参数构造函数,从而使库能够成功实例化并执行 PreCacheTask。
使用 ::class.java 的反射
库可以通过让应用直接传递 Class 对象来加载类,这是一种比按名称加载类更可靠的方法。这会创建对 R8 可以检测到的类的强引用。不过,虽然这可以防止 R8 移除该类,但您仍然需要使用保留规则来声明该类是反射实例化的,并保护以反射方式访问的成员(如构造函数)。
例如,考虑以下场景:您有一个库和一个
使用该库的应用 - 库加载器通过直接传递类引用来实例化 StartupTask
接口。
库代码如下所示:
// The interface for a task that runs once.
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()
}
}
使用该库的应用具有以下代码:
// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
override fun run() {
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
在这种情况下,您的库应包含一个消费者保留规则文件,其中包含以下保留规则:
# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
<init>();
}
这些规则旨在与此类反射完美配合使用,从而在确保代码正常运行的同时实现最大程度的优化。这些规则允许 R8 混淆类名称,并在应用从未使用 StartupTask 类的情况下缩减或移除该类的实现。不过,
对于任何实现(例如示例中使用的 PrecacheTask),
它们都会保留库需要
调用的默认构造函数 (<init>())。
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask:此规则以实现StartupTask接口的任何类为目标。-keep class * implements com.example.library.StartupTask:这会保留实现接口的任何类 (*)。,allowobfuscation:这会指示 R8,即使保留该类,也可以对其进行重命名或混淆处理。这是安全的,因为您的库不依赖于该类的名称;它会直接获取Class对象。,allowshrinking:此修饰符指示 R8,如果该类未使用,则可以将其移除。这有助于 R8 安全地删除从未传递给TaskRunner.execute()的StartupTask实现。简而言之,此规则意味着:如果应用使用实现StartupTask的类,R8 会保留该类。R8 可以重命名该类以减小其大小,并且如果应用未使用该类,则可以将其删除。
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }:此规则以第一个规则中标识的类的特定成员为目标,在本例中为构造函数。-keepclassmembers class * implements com.example.library.StartupTask:这会保留实现StartupTask接口的类的特定成员 (方法、字段),但前提是实现的类本身要保留。{ <init>(); }:这是成员选择器。<init>是 Java 字节码中构造函数的特殊内部名称。此部分专门针对默认的无参数构造函数。- 此规则至关重要,因为您的代码会调用不带任何参数的
getDeclaredConstructor().newInstance(),这会以反射方式调用默认构造函数。如果没有此规则,R8 会发现没有代码直接调用new PreCacheTask(),并假定构造函数未使用,然后将其移除。这会导致您的应用在运行时崩溃并显示InstantiationException。
基于方法注解的反射
库通常定义开发者用于标记方法或字段的注解。
然后,库会在运行时使用反射来查找这些带注解的成员。例如,@OnLifecycleEvent 注解用于在运行时查找所需的方法。
例如,考虑以下场景:您有一个库和一个使用该库的应用 - 该示例演示了一个事件总线,该总线会查找并调用使用 @OnEvent 注解的方法。
库代码如下所示:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent
class EventBus {
fun dispatch(listener: Any) {
// Find all methods annotated with @OnEvent and invoke them
listener::class.java.declaredMethods.forEach { method ->
if (method.isAnnotationPresent(OnEvent::class.java)) {
try {
method.invoke(listener)
} catch (e: Exception) { /* ... */ }
}
}
}
}
使用该库的应用具有以下代码:
class MyEventListener {
@OnEvent
fun onSomethingHappened() {
// This method will be removed by R8 without a keep rule
Log.d(TAG, "Event received!")
}
}
fun onCreate() {
// Instantiate the listener and the event bus
val listener = MyEventListener()
val eventBus = EventBus()
// Dispatch the listener to the event bus
eventBus.dispatch(listener)
}
库应包含一个消费者保留规则文件,该文件会自动保留使用其注解的任何方法:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations:此规则会保留 注解,这些注解旨在运行时读取。-keep @interface com.example.library.OnEvent:此规则会保留OnEvent注解类本身。-keepclassmembers class * {@com.example.library.OnEvent <methods>;}:此规则仅在类正在使用且类包含这些成员时保留类和特定成员。-keepclassmembers:此规则仅在类正在使用且类包含这些成员时保留类和特定成员。class *:该规则适用于任何类。@com.example.library.OnEvent <methods>;:这会保留具有一个或多个使用@com.example.library.OnEvent注解的方法 (<methods>) 的任何类 ,并保留带注解的 方法本身。
基于类注解的反射
库可以使用反射来扫描具有特定
注解的类。在这种情况下,任务运行器类会使用反射查找所有使用 ReflectiveExecutor 注解的类,并执行 execute 方法。
例如,考虑以下场景:您有一个库和一个使用该库的应用。
库具有以下代码:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor
class TaskRunner {
fun process(task: Any) {
val taskClass = task::class.java
if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
val methodToCall = taskClass.getMethod("execute")
methodToCall.invoke(task)
}
}
}
使用该库的应用具有以下代码:
// In consumer app
@ReflectiveExecutor
class ImportantBackgroundTask {
fun execute() {
// This class will be removed by R8 without a keep rule
Log.e("ImportantBackgroundTask", "Executing the important background task...")
}
}
// Usage of ImportantBackgroundTask
fun onCreate(){
val task = ImportantBackgroundTask()
val runner = TaskRunner()
runner.process(task)
}
由于库以反射方式使用反射来获取特定类,因此库应包含一个消费者保留规则文件,其中包含以下保留规则:
# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations
# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor
# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
public void execute();
}
此配置非常高效,因为它会准确告知 R8 要保留的内容。
反射以支持可选依赖项
反射的常见用例是在核心库和可选的插件库之间创建软依赖项。核心库可以检查应用中是否包含插件,如果包含,则可以启用额外的功能。这样,您就可以发布插件模块,而无需强制核心库直接依赖于这些模块。
核心库使用反射 (Class.forName) 按名称查找特定类。如果找到该类,则启用该功能。如果未找到,则会正常失败。
例如,考虑以下代码,其中核心 AnalyticsManager 会检查可选的 VideoEventTracker 类以启用视频分析。
核心库具有以下代码:
object AnalyticsManager {
private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
fun initialize() {
try {
// Attempt to load the optional module's class using reflection
Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
Log.d(TAG, "Video tracking enabled.")
} catch (e: ClassNotFoundException) {
Log.d(TAG,"Video tracking module not found. Skipping.")
} catch (e: Exception) {
Log.e(TAG, e.printStackTrace())
}
}
}
可选的视频库具有以下代码:
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
可选库的开发者负责提供必要的消费者保留规则。此保留规则可确保使用可选库的任何应用都会保留核心库需要查找的代码。
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
如果没有此规则,R8 可能会从可选库中移除 VideoEventTracker,因为该模块中没有任何内容直接使用它。保留规则会保留该类及其构造函数,从而使核心库能够成功实例化它。
反射以访问私有成员
使用反射来访问不属于库的公共 API 的私有或受保护代码可能会带来严重问题。此类代码可能会在不发出通知的情况下发生更改,这可能会导致应用出现意外行为或崩溃。
如果您依赖于反射来访问非公共 API,则可能会遇到以下问题:
- 更新受阻: 私有或受保护代码的更改可能会阻止您更新到更高的库版本。
- 错过优势: 您可能会错过新功能、重要的崩溃修复或必要的安全更新。
R8 优化和反射
如果您必须反射到库的私有或受保护代码中,请密切关注 R8 的优化。如果没有对这些成员的直接引用,R8 可能会假定它们未使用,然后将其移除或重命名。
这可能会导致运行时崩溃,并且通常会显示误导性出错提示,例如 NoSuchMethodException 或 NoSuchFieldException。
例如,考虑以下场景,该场景演示了如何从库类访问私有字段。
您不拥有的库具有以下代码:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
您的应用具有以下代码:
fun accessSecretMessage(instance: LibraryClass) {
// Use Java reflection from Kotlin to access the private field
val secretField = instance::class.java.getDeclaredField("secretMessage")
secretField.isAccessible = true
// This will crash at runtime with R8 enabled
val message = secretField.get(instance) as String
}
在应用中添加 -keep 规则,以防止 R8 移除私有字段:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers:此规则仅在类本身被保留时保留类的特定成员。class com.example.LibraryClass:这以包含该字段的确切类为目标。private java.lang.String secretMessage;:这会按名称和类型标识特定的私有字段。
Java 原生接口 (JNI)
当使用从原生(C/C++ 代码)到 Java 或 Kotlin 的上调时,R8 的优化可能会出现问题。虽然反过来也是如此 - 从 Java 或 Kotlin 到原生代码的下调可能会出现问题 - 但默认文件 proguard-android-optimize.txt 包含以下规则,以保持下调正常运行。此规则可防止原生方法被剪裁。
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
通过 Java 原生接口 (JNI) 与原生代码交互
当您的应用使用 JNI 从原生 (C/C++) 代码到 Java 或 Kotlin 进行上调时,R8 无法看到从原生代码调用的方法。如果您的应用中没有对这些方法的直接引用,R8 会错误地假定这些方法未使用并将其移除,从而导致应用崩溃。
以下示例展示了一个 Kotlin 类,其中包含一个旨在从原生库调用的方法。原生库会实例化应用类型,并将数据从原生代码传递到 Kotlin 代码。
package com.example.models
// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
/**
* This method is called from the native side.
* R8 will remove it if it's not kept.
*/
fun onNativeEvent(data: NativeData) {
Log.d(TAG, "Received event from native code: $data")
}
// Use 'external' to declare a native method
external fun startNativeProcess()
companion object {
init {
// Load the native library
System.loadLibrary("my-native-lib")
}
}
}
在这种情况下,您必须告知 R8,以防止应用类型被优化。此外,如果从原生代码调用的方法在其签名中使用您自己的类作为参数或返回类型,您还必须验证这些类是否未被重命名。
将以下保留规则添加到您的应用中:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
这些保留规则可防止 R8 移除或重命名 onNativeEvent 方法及其参数类型(这一点至关重要)。
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}:此规则仅在类首先在 Kotlin 或 Java 代码中实例化时保留类的特定成员 - 它会告知 R8 应用正在使用该类,并且应保留该类的特定成员。-keepclassmembers:此规则仅在类首先在 Kotlin 或 Java 代码中实例化时保留类的特定成员 - 它会告知 R8 应用正在使用该类,并且应保留该类的特定成员。class com.example.JniBridge:这以包含该字段的确切类为目标。includedescriptorclasses:此修饰符还会保留在方法的签名或描述符中找到的任何类。在这种情况下,它可以防止 R8 重命名或移除用作参数的com.example.models.NativeData类。如果NativeData被重命名(例如,重命名为a.a),则方法签名将不再与原生代码的预期匹配,从而导致崩溃。public void onNativeEvent(com.example.models.NativeData);:这会指定要保留的方法的确切 Java 签名。
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}:虽然includedescriptorclasses可确保保留NativeData类本身,但需要从原生 JNI 代码直接访问的NativeData中的任何成员(字段或方法)都需要自己的保留规则。-keep class NativeData:这以名为NativeData的类为目标,并且该块指定要保留NativeData类中的 哪些成员。<init>(java.lang.Integer, java.lang.String):这是 构造函数的签名。它唯一标识了接受两个参数的构造函数:第一个是Integer,第二个是String。
您还可以使用 Jetpack Keep 中的
@UnconditionallyKeep注解来注解原生代码调用的方法,而无需为其编写保留
规则。该注解不会保留方法签名中的类(例如 NativeData),因此您仍需要单独保留这些类。
间接平台调用
使用 Parcelable 的实现来传输数据
Android 框架使用反射来创建 Parcelable 对象的实例。在现代 Kotlin 开发中,您应使用 kotlin-parcelize 插件,该插件会自动生成必要的 Parcelable 实现,包括框架需要的 CREATOR 字段和方法。
例如,考虑以下示例,其中使用 kotlin-parcelize 插件创建 Parcelable 类:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
val name: String,
val age: Int
) : Parcelable
在这种情况下,没有推荐的保留规则。kotlin-parcelize Gradle 插件会自动为使用 @Parcelize 注解的类生成所需的保留规则。它会为您处理复杂性,确保为 Android 框架的反射调用保留生成的 CREATOR 和构造函数。
如果您在 Kotlin 中手动编写 Parcelable 类而不使用 @Parcelize,则需要负责保留 CREATOR 字段和接受 Parcel 的构造函数。如果您忘记这样做,则当系统尝试反序列化您的对象时,应用会崩溃。使用 @Parcelize 是标准且更安全的做法。
使用 kotlin-parcelize 插件时,请注意以下事项:
- 该插件会在编译期间自动创建
CREATOR字段。 proguard-android-optimize.txt文件包含必要的keep规则,以保留这些字段以实现正常功能。- 应用开发者必须验证是否存在所有必需的
keep规则,尤其是对于任何自定义实现或第三方依赖项。
热门库
使用反射或字节码转换的库会在运行时动态访问代码。如果 R8 移除或重命名以这种方式访问的类、字段或方法,您的应用可能会崩溃。
不过,热门的第三方库(例如 Gson、Retrofit 和 Kotlinx Serialization)会自动捆绑自己的 R8 消费者保留规则。使用这些库的最新版本时,您无需向项目中添加手动保留规则。
Gson
Gson 是一个 JSON 序列化和反序列化库,它严重依赖于反射。当您使用完整模式优化应用时,它会剥离泛型 类型签名、默认构造函数和未注解的字段,除非 另有明确说明。
为确保 Gson 正常运行,请添加特定规则以保留数据模型类中的非瞬态字段,并保留 TypeToken 层次结构:
# Preserve generic type information required for deserialization
-keepattributes Signature
# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
!transient <fields>;
}
# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
使用 transient 修饰符标记的字段在序列化和反序列化期间会被 Gson 忽略,这就是保留规则专门针对非瞬态字段 (!transient) 的原因。
Retrofit
Retrofit 是一个网络库,它使用反射来检查使用 HTTP 注解(例如 @GET 或 @POST)注解的服务接口方法,以构建网络请求和转换响应。
Retrofit 会在运行时使用 Proxy.newProxyInstance() 动态生成 API 接口的实现。由于 R8 不会静态实现这些接口,因此可能会剥离这些方法或其泛型返回值类型。
捆绑的保留规则
Retrofit 依赖于运行时反射来检查泛型参数、方法注解和参数注解。如果没有正确的配置,R8 完整模式可以彻底剥离返回值类型、Kotlin 延续和响应类中的泛型签名,甚至将接口值替换为 null,因为 Retrofit 接口是使用代理动态实例化的。
从 Retrofit 2.10.0 开始,该库会自动捆绑保留注解默认值、服务方法参数和必要类元数据所需的官方保留规则。如需了解详情,请参阅 Retrofit 使用的规则。
保留泛型返回值类型
Retrofit 会检查返回值类型的泛型签名(例如,
Observable<Data>)以正确反序列化网络响应。如果 R8 剥离泛型签名,Retrofit 会将实例化的对象替换为 null。
为防止 R8 完整模式剥离返回值类型的泛型签名,请使用以下条件规则:
# Preserve generic type information for Call/Observable return types
-keepattributes Signature
# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
@retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
还必须保留实际返回的数据模型类(例如,Data 在
Observable<Data> 中),因为它将由
转换器(例如 Gson)以反射方式构建。
协程
当您使用 Kotlin 协程时,Kotlin 编译器会通过将 Continuation 参数附加到已编译的方法签名来转换 suspend 函数。
当 Retrofit 等库以反射方式读取 suspend 函数的泛型签名时,它们依赖于该 Continuation 参数。使用完整模式时,仅为显式保留的类保留 Signature 属性。由于 Continuation 是合成参数,因此 R8 默认会剥离其签名,从而破坏反射。
为防止签名剥离并确保完整模式下的运行时兼容性,请添加以下规则:
# Keep the signature attribute globally
-keepattributes Signature
# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation