借助 Jetpack Keep,您可以直接在 Kotlin 或 Java 源代码中使用注解声明反射和 Java 原生接口 (JNI) 用法,而无需在单独的保留规则文件中声明。Jetpack Keep Gradle 插件会在构建时读取这些注解,并生成 R8 所需的 Keep 规则。
Jetpack Keep 可帮助 R8 优化在代码库发生变化时保持有效:
保留规则应紧邻需要它们的代码。当您重命名或删除使用反射的代码时,您需要在同一位置更新注解。没有单独的规则文件需要保持同步。
生成的规则是有条件的。只有在 R8 保留发出反射调用的代码时,才会保留反射调用的目标。如果调用代码未被使用,并且 R8 将其移除(通常发生在可选库功能中),R8 也可以移除目标,这样未使用的功能的规则就不会增加代码膨胀。
Lint 会为您查找反射。该库包含 lint 检查,可标记未添加注解的反射调用,并可自动添加许多注解。
Jetpack Keep 的运作方式
假设有一个函数,它会按名称实例化类。如果 R8 无法判断您的代码是否使用了该类,则会移除该类的构造函数,导致应用在运行时崩溃。为防止发生崩溃,您可以向保留规则文件添加如下规则:
# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
<init>();
}
借助 Jetpack Keep,您可以改为为执行反射的函数添加注解:
// ResourceLocatorImpl isn't on this module's compile classpath, so both the
// annotation and the code refer to it by name.
@UsesReflectionToConstruct(
className = "com.example.ResourceLocatorImpl",
// Omitting parameterTypes keeps every constructor. An empty array keeps
// only the no-argument constructor, which the code calls.
parameterTypes = [],
)
fun loadResourceLocator(): ResourceLocator {
val implClass = Class.forName("com.example.ResourceLocatorImpl")
return implClass.getDeclaredConstructor().newInstance() as ResourceLocator
}
在构建时,Gradle 插件会为构造函数生成一条保留规则。
与前面的手动编写规则不同,生成的规则是有条件的:只有当 loadResourceLocator 保留在优化后的应用中时,R8 才会保留构造函数。
要求
如需使用 Jetpack Keep,您的项目需要使用 Android Gradle 插件 (AGP) 8.7 或更高版本。
Gradle 插件支持以下模块类型:
| 模块类型 | 模块应用的插件 | 其他设置 |
|---|---|---|
| Android 应用 | com.android.application |
无 |
| Android 库 | com.android.library |
无 |
| 具有 Android 目标的 Kotlin Multiplatform 库 | com.android.kotlin.multiplatform.library |
无 |
| JVM 库(Java 或 Kotlin) | java-library 或 org.jetbrains.kotlin.jvm |
注册 JAR 转换 |
设置 Jetpack Keep
如需使用 Jetpack Keep,请将该库和 Gradle 插件添加到每个使用反射或 JNI 的模块中。该库提供注解和 lint 检查,而插件会将注解转换为保留规则。
在
libs.versions.toml文件中,添加库和插件。该库和插件具有相同的版本:[versions] # The library and the plugin ship together. Use one version for both so # that they stay in sync when you upgrade. annotationKeep = "1.0.0-alpha01" [libraries] androidx-annotation-keep = { group = "androidx.annotation.keep", name = "keep", version.ref = "annotationKeep" } [plugins] androidx-annotation-keep = { id = "androidx.annotation.keep", version.ref = "annotationKeep" }Gradle 会从 Google 的 Maven 仓库下载该插件,因此请确保
settings.gradle.kts文件中的pluginManagement仓库包含google()。在根
build.gradle.kts文件中,声明该插件以及 AGP,但不应用它:plugins { alias(libs.plugins.android.application) apply false // Other plugins. // The plugin relies on AGP classes that it doesn't bundle, so declare // it next to AGP. Declaring it here also gives every module the same // plugin version. alias(libs.plugins.androidx.annotation.keep) apply false }在每个使用反射或 JNI 的模块的
build.gradle.kts文件中,应用该插件并添加库依赖项:plugins { alias(libs.plugins.android.application) // The plugin reads annotations only from this module's own code, so // apply it in every module that uses Jetpack Keep. alias(libs.plugins.androidx.annotation.keep) } dependencies { implementation(libs.androidx.annotation.keep) }将项目与 Gradle 文件同步。
对于 Android 应用模块的每个 build 变体,该插件都会注册一个 <variant>ExtractKeepRules 任务(例如 releaseExtractKeepRules),并将生成的规则传递给该变体的 R8。对于库模块,该插件会将生成的规则打包为库输出中的使用方 keep 规则。
JVM 库模块需要额外设置。
使用 lint 查找反射
Jetpack Keep 包含以下 lint 检查:
ReflectionWithoutKeepAnnotations- 标记没有匹配的 keep 注解的反射调用,例如
Class.forName、getDeclaredConstructor、getDeclaredMethod和getDeclaredField。 MissingKeepAnnotationPlugin- 标记依赖于
androidx.annotation.keep:keep但不应用androidx.annotation.keepGradle 插件的模块。如果没有该插件,您的 build 就不会根据注解生成保留规则。
如需使用 lint 为 Android 模块添加注解,请按以下步骤操作:
对模块运行 lint,以查找需要注解的反射调用:
./gradlew :app:lint应用 lint 可以自动进行的修复:
./gradlew :app:lintFix当 Lint 可以确定代码反映的类和成员时,它可以自动添加注解。
检查每个添加的注解,确认其与代码执行的操作一致。例如,检查参数类型是否与代码调用的构造函数或方法匹配。
在 Android Studio 中解决其余发现结果。当 lint 无法确定目标类时,快速修复会插入带有
TODO占位符的注解,供您完成。您也可以手动添加注解。例如,以下函数接收类名称作为参数,因此 lint 无法确定它构造的是哪个类。快速修复会添加一个带有占位符的注解:
@UsesReflectionToConstruct( className = TODO(), parameterTypes = [] ) fun createInstance(className: String): Any = Class.forName(className).getDeclaredConstructor().newInstance()将占位符替换为代码构造的类。如果该类在编译时可用,请使用
classConstant而不是className。在 Kotlin 中,只有替换TODO(),代码才能编译。在移除任何现有规则之前,请先检查插件根据您的注解生成了哪些保留规则。对于应用模块,请针对发布变体运行提取任务:
./gradlew :app:releaseExtractKeepRules该插件会将规则写入
app/build/generated/release/androidx.annotation.keep.rules.pro。对于库模块,请参阅验证生成的规则。从保留规则文件中移除被新注解替换的保留规则。规则可能还涵盖 lint 未标记的反射,例如其他模块或库中的反射,因此请在移除每条规则之前进行检查:
- 对于包级规则(例如
-keep class com.example.** { *; }),请确保注解或其他规则涵盖对包中类的所有反射。 - 对于保留特定类或成员的规则,请确保它与生成的某个规则紧密匹配,或者没有其他反射依赖于它。
- 对于包级规则(例如
测试发布 build。
为代码添加注解
向执行反射的代码(包含反射调用的函数、构造函数、字段或类)添加 Jetpack Keep 注解。注解描述了反射的目标,并且仅当 R8 保留带注解的代码时,生成的规则才会保留该目标。
根据代码的功能选择注解:
| 注释 | 在以下情况下使用:您的代码 |
|---|---|
@UsesReflectionToConstruct |
通过反射调用构造函数,例如使用 getDeclaredConstructor().newInstance()。 |
@UsesReflectionToAccessMethod |
通过反射调用方法,例如使用 getMethod 或 getDeclaredMethod。 |
@UsesReflectionToAccessField |
通过反射读取或写入字段,例如使用 getField 或 getDeclaredField。 |
@UnconditionallyKeep |
从应用的 Kotlin 或 Java 代码外部调用,例如通过 JNI 从原生代码调用。仅当您无法使用 @UsesReflection* 注释(会生成条件规则)时,才使用此注释。 |
所有 @UsesReflection* 注解都遵循以下惯例:
- 使用
classConstant或className标识目标类。如果该类在编译时可用,请使用classConstant。编译器会检查引用,而 IDE 重构(例如重命名)会更新引用。仅当类在编译时不可用(例如可选依赖项中的类)时,才使用className。这两个参数是互斥的。 - 定位到最具体的类。生成的规则会保留您指定的类及其子类或接口实现中的匹配成员。类越通用,R8 保留的代码就越多。
- 指定参数类型以保留单个重载。如果您未设置
parameterTypes或parameterTypeNames,规则会保留方法的所有构造函数或所有重载。如需仅保留无参数构造函数或方法,请传递空数组。 - 为多个目标重复添加注解。如果一个函数反映了多个类或成员,请为每个目标添加一个注解。
使用反射构造对象
当您的代码通过反射实例化类时,请使用 @UsesReflectionToConstruct。在以下示例中,任务运行器按名称实例化 StartupTask 接口的实现。由于注解指定了接口,因此只要 R8 保留 execute,生成的规则就会保留每个 StartupTask 实现的无参数构造函数:
interface StartupTask {
fun run()
}
object TaskRunner {
// The task class is known only at runtime, so target the interface that
// every task implements.
@UsesReflectionToConstruct(classConstant = StartupTask::class, parameterTypes = [])
fun execute(className: String) {
val task = Class.forName(className)
.getDeclaredConstructor()
.newInstance() as StartupTask
task.run()
}
}
使用反射调用方法
当您的代码通过反射调用方法时,请使用 @UsesReflectionToAccessMethod。将 methodName 设置为方法的名称。在以下示例中,代码会调用可选依赖项中的静态方法。该类在编译时间不可用,因此注解使用 className 而不是 classConstant:
object CrashReporting {
@UsesReflectionToAccessMethod(
className = "com.example.crashlib.CrashReporter",
methodName = "log",
// Omitting parameterTypes keeps every log overload. Keep only the one
// that the code calls.
parameterTypes = [String::class],
)
fun log(message: String) {
val reporterClass = try {
Class.forName("com.example.crashlib.CrashReporter")
} catch (e: ClassNotFoundException) {
// Crash reporting is optional, so skip it instead of failing.
return
}
reporterClass.getMethod("log", String::class.java).invoke(null, message)
}
}
使用反射访问字段
当您的代码通过反射读取或写入字段时,请使用 @UsesReflectionToAccessField。将 fieldName 设置为字段名称:
class UserProfileFetcher {
// User doesn't expose internalId, so reading it requires reflection.
@UsesReflectionToAccessField(classConstant = User::class, fieldName = "internalId")
fun readInternalId(user: User): Any? {
val field = User::class.java.getDeclaredField("internalId")
field.isAccessible = true
return field.get(user)
}
}
保留从应用外部调用的代码
对于从应用的 Kotlin 或 Java 代码外部调用的代码(例如原生代码通过 JNI 调用的方法),请使用 @UnconditionallyKeep。请为被调用的元素添加注解,而不是调用该元素的代码:
class NativeBridge {
// Only native code calls this method, through a JNI lookup by name that
// R8 can't see. Keep the method and its name unconditionally.
@UnconditionallyKeep
fun onNativeEvent(message: String) {
Log.d("NativeBridge", "Received from native code: $message")
}
// The default proguard-android-optimize.txt file already keeps native
// method names, so this method doesn't need an annotation.
external fun startNativeProcess()
}
生成的规则会保留带注解的方法及其包含类,并保留其原始名称。它不会保留该方法的签名引用的其他类。如果原生代码依赖于这些类,也请为它们添加注解,或为它们添加保留规则。如需详细了解 JNI 和 R8,请参阅通过 JNI 与原生代码交互。
默认情况下,@UnconditionallyKeep 会保留元素的名称。如果外部调用方不依赖于名称,请设置 shouldPreserveName = false,以便 R8 可以重命名元素。
验证生成的规则
本部分介绍了如何检查为每种类型的模块生成的规则。 对于应用模块,插件会将规则直接传递给 R8。对于库模块,插件会将这些规则打包为库输出中的使用方 keep 规则。依赖于您的库的应用在运行 R8 时会自动应用这些规则,即使这些应用不使用 Jetpack Keep 也是如此。如需详细了解使用方保留规则,请参阅针对库作者的优化。
Android 应用
如需检查插件为应用模块生成的保留规则,请针对 build 变体运行提取任务:
./gradlew :app:releaseExtractKeepRules
该插件会将每个变体的规则写入 app/build/generated/<variant>/androidx.annotation.keep.rules.pro。每条规则前面的 # context: 注释用于标识生成该规则的带注解代码。
如需检查生成的规则如何与其他保留规则一起影响优化,请使用 R8 配置分析器。
Android 库
Android 库模块不需要任何额外设置。当您构建 AAR 时,插件会将生成的规则添加到 AAR 的 proguard.txt 文件中,并放在 AAR 已包含的所有使用者保留规则之后。为此,该插件会为每个 build 变体注册一个 <variant>KeepRulesTransformAar 任务。
如需验证规则,请构建 AAR(例如使用 ./gradlew
:mylibrary:assembleRelease),然后在生成的 AAR 中打开 proguard.txt。生成的规则会显示在具有以下标题的部分中:
#################################################################
# Rules generated from androidx.annotation.keep annotations #
#################################################################
在 macOS 和 Linux 上,您还可以通过命令行输出规则:
unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt
Kotlin Multiplatform 库
具有 Android 目标的 Kotlin Multiplatform 库模块也不需要任何额外设置。当您为 Android 目标构建 AAR 时,该插件会以与 Android 库相同的方式将生成的规则添加到 AAR 的 proguard.txt 文件中。该插件会注册一个 androidMainKeepRulesTransformAar 任务来执行此操作。
如需验证规则,请运行该任务,并在其创建的 AAR 中打开 proguard.txt。在 macOS 和 Linux 上,您可以通过命令行输出规则:
./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt
JVM 库
您必须先配置 JVM 库模块,然后才能验证该模块的规则,以便插件将生成的规则添加到其 JAR 的副本中。然后,运行您注册的转换任务,并检查它创建的 JAR 中的规则文件。例如,如果您将任务命名为 keepRulesJar,则可以使用以下命令在 macOS 和 Linux 上输出规则:
./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
"META-INF/com.android.tools/*" "META-INF/proguard/*"
配置 JVM 库
对于生成 JAR 的 JVM 库模块,该插件不会自动修改 JAR。请改用 annotationKeep.registerJavaArchiveTransform 函数注册一个任务,该任务会使用生成的规则创建 JAR 的副本,然后发布该 JAR。将以下代码添加到库模块的 build.gradle.kts 文件中:
plugins {
id("java-library")
alias(libs.plugins.jetbrains.kotlin.jvm)
alias(libs.plugins.androidx.annotation.keep)
}
val jarTask = tasks.named<Jar>("jar")
// Apps run R8 on this library's code, so the generated rules must be inside
// the JAR. The plugin handles this automatically only for Android modules.
val keepRulesJarTask = annotationKeep.registerJavaArchiveTransform(
taskName = "keepRulesJar",
input = jarTask.flatMap { it.archiveFile },
fileName = jarTask.flatMap { it.archiveFileName }.map { it.replace(".jar", "-keep-rules.jar") },
)
// Consumers of this library resolve apiElements and runtimeElements, so add
// the JAR that contains the generated rules to them.
configurations.configureEach {
if (name == "apiElements" || name == "runtimeElements") {
outgoing.artifact(keepRulesJarTask.flatMap { it.outputJar })
}
}
dependencies {
implementation(libs.androidx.annotation.keep)
}
该转换会将生成的规则添加到 JAR 的目标 R8 规则位置 META-INF/com.android.tools/r8/proguard.txt。如果 JAR 已包含 META-INF/com.android.tools/ 或 META-INF/proguard/ 中的规则,则转换会将生成的规则附加到现有规则文件中。如需详细了解这些位置,请参阅支持不同的 R8 版本。
注册转换后,验证 JAR 中的规则。
示例:加载可选的插件库
库通常使用反射来加载可选插件。例如,Analytics SDK 可能会提供核心库和视频插件。插件依赖于核心库,因此核心库无法直接引用插件的类。相反,核心库会在运行时按名称查找插件的类,并且仅在找到该类时才会启用视频分析。
由于核心库仅通过反射使用插件的类,因此 R8 可能会从应用的发布 build 中移除该类或其构造函数,从而导致查找失败。此故障仅发生在包含该插件的应用的发布 build 中,因此在测试中很难发现。
如果不使用 Jetpack Keep,插件会为其类提供一个使用方保留规则,如通过反射支持可选依赖项中所示:
# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
此规则与需要它的查询位于不同的库中,因此两者可能会不同步。借助 Jetpack Keep,您可以在核心库中为查找添加注解:
// In the core library.
interface AnalyticsPlugin {
fun start()
}
object AnalyticsManager {
// The annotation and the lookup share this constant so that they can't
// refer to different classes.
private const val VIDEO_TRACKER_CLASS =
"com.example.analytics.video.VideoEventTracker"
@UsesReflectionToConstruct(
className = VIDEO_TRACKER_CLASS,
parameterTypes = [],
)
fun initialize() {
val tracker = try {
Class.forName(VIDEO_TRACKER_CLASS)
.getDeclaredConstructor()
.newInstance() as AnalyticsPlugin
} catch (e: ClassNotFoundException) {
// Apps that don't play video leave out the add-on, so a missing
// class isn't an error.
return
}
// This is an ordinary interface call, not a reflective one, so R8
// keeps start() without an annotation.
tracker.start()
}
}
注解遵循以下准则:
- 为执行反射的代码添加注解。注解位于调用
Class.forName的initialize上,而不是位于插件的类上。 - 根据代码的功能选择注解。该查找会创建类的实例,因此代码会使用
@UsesReflectionToConstruct。 - 对无法引用的类使用
className。插件的类不在核心库的编译类路径中,因此注解无法使用classConstant。 - 仅保留代码调用的构造函数。该查找会调用无参数构造函数,因此
parameterTypes是空数组。
在您构建核心库的 AAR 时,插件会添加一条规则,以便在 R8 保留 initialize 时保留 VideoEventTracker 及其无参数构造函数。使用该 SDK 的应用会自动应用此规则,即使它们不使用 Jetpack Keep 也是如此,因此该插件不需要自己的规则。
与插件的规则相比,此方法具有以下优势:
- 规则会保留在查找旁边。核心库同时拥有查找和规则,因此您可以同时更新它们,而无需协调两个库之间的更改。
- R8 仍可移除未使用的代码。如果应用包含该插件,但从未调用
initialize,则生成的规则不适用,R8 可以移除VideoEventTracker。该插件的规则会将其保留在包含该插件的每个应用中。 - Lint 会捕获未添加注解的查询。如果核心库稍后添加了另一个没有注解的反射查找,lint 会将其标记出来,以便您在发布库之前进行修复。
如需确认结果,请检查生成的规则是否显示在 AAR 的 proguard.txt 文件中,如 Android 库中所述。然后,测试包含该插件的应用的发布 build,因为缺少规则会导致视频分析功能无法正常运行。
限制
Jetpack Keep 尚无法检测通过注释查找类和成员的反射,也无法为其生成保留规则。此功能已列入计划。
在此之前,请使用保留规则来处理此类反射。例如,以下规则会保留 R8 保留的类中带有 @SomeAnnotation 注解的每个成员:
-keepclassmembers class * {
@com.example.annotation.SomeAnnotation <methods>;
@com.example.annotation.SomeAnnotation <fields>;
}
如果 Jetpack Keep 不适用于您的用例,或者您有其他反馈,请提交问题。
其他资源
- Jetpack Keep 示例:一个 Android Studio 项目,在应用、Android 库、Kotlin Multiplatform 库和 JVM 库中使用 Jetpack Keep。
- 保留规则用例和示例:常见反射和 JNI 场景的保留规则。
- 遵循最佳实践:有关编写范围较窄的保留规则的指南。