借助 Jetpack Keep,您可以使用注解直接在 Kotlin 或 Java 源代码中声明反射和 Java 本地接口 (JNI) 用法,而无需在单独的 keep 规则文件中进行声明。Jetpack Keep Gradle 插件会在构建时读取这些注解,并生成 R8 所需的 keep 规则。
Jetpack Keep 有助于确保 R8 优化在代码库发生变化时保持有效:
keep 规则与需要它们的代码保持相邻。当您重命名或删除使用反射的代码时,您可以在同一位置更新注解。无需单独的文件来保持规则同步。
生成的规则是有条件的。R8 仅在保留发出反射调用的代码时,才会保留反射调用的目标。如果调用代码未使用,并且 R8 移除了该代码(在可选库功能中经常出现这种情况),R8 也可以移除目标,这样未使用功能的规则就不会增加代码膨胀。
Lint 会为您查找反射。该库包含 lint 检查,这些检查会标记未添加注解的反射调用,并且可以自动添加许多注解。
Jetpack Keep 的工作原理
假设有一个函数按名称实例化一个类。如果 R8 无法判断您的代码是否使用了该类,它会移除该类的构造函数,并且应用会在运行时崩溃。为防止崩溃,您可以向 keep 规则文件添加如下规则:
# 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 插件会为构造函数生成 keep 规则。
与前面手动编写的规则不同,生成的规则是有条件的:R8 仅在 loadResourceLocator 仍保留在优化后的应用中时,才会保留构造函数。
要求
如需使用 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 检查,而插件会将注解转换为 keep 规则。
在
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 就不会根据注解生成 keep 规则。
如需使用 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()后,代码才会编译。在移除任何现有规则之前,检查插件根据注解生成的 keep 规则。对于应用模块,请为发布变体运行提取任务:
./gradlew :app:releaseExtractKeepRules插件会将规则写入
app/build/generated/release/androidx.annotation.keep.rules.pro。对于 库模块,请参阅验证生成的规则。从 keep 规则文件中移除新注解替换的 keep 规则。规则还可能涵盖 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 接口的实现。由于注解指定了接口,因此生成的规则会保留每个 StartupTask 实现的无参数构造函数,前提是 R8 保留 execute:
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()
}
生成的规则会保留带注解的方法及其包含类,并保留其原始名称。它不会保留方法的签名引用的其他类。如果原生代码依赖于这些类,请也为它们添加注解,或为它们添加 keep 规则。如需详细了解 JNI 和 R8,请参阅通过 JNI 与原生代码 互动。
默认情况下,@UnconditionallyKeep 会保留元素的名称。如果外部调用方不依赖于该名称,请设置 shouldPreserveName = false,以便 R8 可以重命名该元素。
验证生成的规则
本部分介绍了如何检查每种模块类型的生成规则。 对于应用模块,插件会将规则直接传递给 R8。对于库模块,插件会将规则打包为库输出中的使用者 keep 规则。依赖于您的库的应用在运行 R8 时会自动应用这些规则,即使这些应用未使用 Jetpack Keep 也是如此。如需详细了解使用者 keep 规则,请参阅 为库作者进行优化。
Android 应用
如需检查插件为应用模块生成的 keep 规则,请为 build 变体运行提取任务:
./gradlew :app:releaseExtractKeepRules
插件会将每个变体的规则写入
app/build/generated/<variant>/androidx.annotation.keep.rules.pro。每条规则之前的 # context: 注释用于标识生成该规则的带注解的代码。
如需检查生成的规则如何与其他 keep 规则一起影响优化,请使用 R8 配置分析器。
Android 库
Android 库模块不需要进行任何其他设置。当您构建 AAR 时,插件会将生成的规则添加到 AAR 的 proguard.txt 文件中,并放在 AAR 已包含的任何使用者 keep 规则之后。插件会为每个 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,附加组件会为其类附带使用者 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 示例:一个 Android Studio 项目,该项目在应用、Android 库、Kotlin Multiplatform 库和 JVM 库中使用 Jetpack Keep。
- keep 规则使用场景和示例:适用于常见反射和 JNI 场景的 keep 规则。
- 遵循最佳实践:有关编写范围狭窄的 keep 规则的指南。