根據註解產生保留規則

Jetpack Keep 可讓您直接在 Kotlin 或 Java 原始碼中,使用註解宣告反射和 Java 原生介面 (JNI) 的使用方式,而不必在個別的保留規則檔案中宣告。Jetpack Keep Gradle 外掛程式會在建構時讀取這些註解,並產生 R8 所需的保留規則。

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 檢查,外掛程式則會將註解轉換為保留規則。

  1. 在 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()。

  2. 在根層級的 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
    }
    
  3. 在每個使用反射或 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)
    }
    
  4. 將專案與 Gradle 檔案同步處理。

針對 Android 應用程式模組的每個建構變化版本,外掛程式會註冊 <variant>ExtractKeepRules 工作 (例如 releaseExtractKeepRules),並將產生的規則傳遞至該變化版本的 R8。如果是程式庫模組,外掛程式會將產生的規則封裝為程式庫輸出內容中的消費者保留規則。JVM 程式庫模組需要額外設定。

使用 Lint 尋找反射

Jetpack Keep 包含下列 Lint 檢查:

ReflectionWithoutKeepAnnotations
標記沒有相符保留註解的反思呼叫,例如 Class.forName、getDeclaredConstructor、getDeclaredMethod 和 getDeclaredField。
MissingKeepAnnotationPlugin
標記依附於 androidx.annotation.keep:keep 但未套用 androidx.annotation.keep Gradle 外掛程式的模組。如果沒有外掛程式,建構作業就不會從註解產生保留規則。

如要使用 Lint 為 Android 模組加上註解,請按照下列步驟操作:

  1. 在模組上執行 Lint,找出需要註解的反射呼叫:

    ./gradlew :app:lint
    
  2. 套用 Lint 可自動進行的修正:

    ./gradlew :app:lintFix
    

    Lint 能夠判斷程式碼反映的類別和成員時,可以自動新增註解。

  3. 請檢查每個新增的註解,確認與程式碼的作用相符。舉例來說,請檢查參數類型是否與程式碼呼叫的建構函式或方法相符。

  4. 在 Android Studio 中解決其餘發現項目。如果 Lint 無法判斷目標類別,快速修正功能會插入含有 TODO 預留位置的註解,供您填寫。您也可以手動新增註解。

    舉例來說,下列函式會接收類別名稱做為參數,因此 Lint 無法判斷建構的是哪個類別。快速修正功能會新增含有預留位置的註解:

    @UsesReflectionToConstruct(
        className = TODO(),
        parameterTypes = []
    )
    fun createInstance(className: String): Any =
        Class.forName(className).getDeclaredConstructor().newInstance()
    

    將預留位置替換為程式碼建構的類別。如果類別在編譯時間可用,請使用 classConstant,而非 className。在 Kotlin 中,您必須替換 TODO(),程式碼才能編譯。

  5. 移除任何現有規則前,請先檢查外掛程式根據註解產生的保留規則。如果是應用程式模組,請針對發布變化版本執行擷取作業:

    ./gradlew :app:releaseExtractKeepRules
    

    外掛程式會將規則寫入 app/build/generated/release/androidx.annotation.keep.rules.pro。如果是程式庫模組,請參閱「驗證產生的規則」。

  6. 從保留規則檔案中,移除新註解取代的保留規則。規則也可能涵蓋 Lint 未標記的反射,例如其他模組或程式庫中的反射,因此請先檢查每個規則,再移除:

    • 如果是套件層級規則 (例如 -keep class com.example.** { *; }),請確認註解或其他規則涵蓋套件類別的所有反射。
    • 如果規則會保留特定類別或成員,請確保該規則與其中一項產生的規則密切相符,或沒有其他反射依賴該規則。
  7. 測試發布子版本。

為程式碼加上註解

在執行反射的程式碼中新增 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。如果是程式庫模組,外掛程式會將規則封裝為程式庫輸出內容中的消費者保留規則。依附您程式庫的應用程式在執行 R8 時,會自動套用這些規則,即使應用程式未使用 Jetpack Keep 亦然。如要進一步瞭解消費者保留規則,請參閱「程式庫作者最佳化」。

Android 應用程式

如要查看外掛程式為應用程式模組產生的保留規則,請為建構變化版本執行擷取工作:

./gradlew :app:releaseExtractKeepRules

外掛程式會將每個變體的規則寫入 app/build/generated/<variant>/androidx.annotation.keep.rules.pro。每項規則前的 # context: 註解會指出產生該規則的註解程式碼。

如要查看產生的規則如何與其他保留規則共同影響最佳化,請使用 R8 設定分析器。

Android 程式庫

Android 程式庫模組不需要任何額外設定。建構 AAR 時,外掛程式會將產生的規則新增至 AAR 的 proguard.txt 檔案,並放在 AAR 已包含的任何消費者保留規則之後。外掛程式會為每個建構變化版本註冊 <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 程式庫

如果 Kotlin Multiplatform 程式庫模組以 Android 為目標,也不需要任何額外設定。為 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 可能會從應用程式的發布子版本中移除該類別或其建構函式,導致查閱失敗。只有在包含外掛程式的應用程式發布子版本中,才會發生失敗,因此在測試中很難發現。

如果沒有 Jetpack Keep,外掛程式會為其類別傳送消費者保留規則,如「Reflection to support optional dependencies」所示:

# 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()
    }
}

註解遵循下列規範:

  • 為執行反射的程式碼加上註解。註解位於 initialize 上,會呼叫 Class.forName,而非外掛程式的類別。
  • 根據程式碼的作用選擇註解。查閱會建立類別的例項,因此程式碼會使用 @UsesReflectionToConstruct。
  • 使用 className 處理無法參照的類別。外掛程式的類別不在核心程式庫的編譯類別路徑中,因此註解無法使用 classConstant。
  • 只保留程式碼呼叫的建構函式。查閱會呼叫無引數建構函式,因此 parameterTypes 是空陣列。

建構核心程式庫的 AAR 時,外掛程式會新增一項規則,只要 R8 保留 initialize,就會保留 VideoEventTracker 及其無引數建構函式。即使應用程式未使用 Jetpack Keep,也會自動套用這項規則,因此外掛程式不需要自己的規則。相較於外掛程式的規則,這種做法有以下優點:

  • 規則會保留在查詢旁邊。核心程式庫同時擁有查閱和規則,因此您會一併更新這兩者,不必協調兩個程式庫的變更。
  • R8 仍可移除未使用的程式碼。如果應用程式包含外掛程式,但從未呼叫 initialize,則產生的規則不適用,R8 可以移除 VideoEventTracker。外掛程式的規則會將其保留在所有包含該外掛程式的應用程式中。
  • Lint 會找出未加註解的查詢。如果核心程式庫之後新增另一個沒有註解的反射查詢,Lint 會標記該查詢,以便您在發布程式庫前修正。

如要確認結果,請檢查產生的規則是否顯示在 AAR 的 proguard.txt 檔案中,如「Android 程式庫」一節所述。然後測試包含外掛程式的應用程式發布子版本,因為缺少規則會導致影片數據分析中斷。

其他資源