Aufbewahrungsregeln aus Anmerkungen generieren

Mit Jetpack Keep können Sie die Verwendung von Reflexion und Java Native Interface (JNI) mit Annotationen direkt in Ihrem Kotlin- oder Java-Quellcode deklarieren, anstatt in separaten Keep-Regeldateien. Das Jetpack Keep Gradle-Plug-in liest diese Annotationen während des Builds und generiert die Keep-Regeln, die R8 benötigt.

Jetpack Keep sorgt dafür, dass die R8-Optimierung auch bei Änderungen an der Codebasis effektiv bleibt:

  • Keep-Regeln bleiben neben dem Code, für den sie erforderlich sind. Wenn Sie Code umbenennen oder löschen, der Reflexion verwendet, aktualisieren Sie die Annotation an derselben Stelle. Es gibt keine separate Regeldatei, die synchronisiert werden muss.

  • Generierte Regeln sind bedingt. R8 behält das Ziel eines reflektiven Aufrufs nur bei, wenn auch der Code beibehalten wird, der den Aufruf ausführt. Wenn der aufrufende Code nicht verwendet und von R8 entfernt wird (häufig bei optionalen Bibliotheksfunktionen der Fall), kann R8 auch das Ziel entfernen, sodass Regeln aus nicht verwendeten Funktionen nicht zu unnötigem Code führen.

  • Lint findet Reflexionen für Sie. Die Bibliothek enthält Lint-Prüfungen, die reflektive Aufrufe ohne Annotationen kennzeichnen und viele der Annotationen automatisch hinzufügen können.

So funktioniert Jetpack Keep

Betrachten wir eine Funktion, die eine Klasse anhand des Namens instanziiert. Wenn R8 nicht erkennen kann, dass Ihr Code die Klasse verwendet, wird der Konstruktor der Klasse entfernt und die App stürzt zur Laufzeit ab. Um den Absturz zu verhindern, können Sie Ihrer Datei mit Beibehaltungsregeln eine Regel wie die folgende hinzufügen:

# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
    <init>();
}

Mit Jetpack Keep annotieren Sie stattdessen die Funktion, die die Reflexion ausführt:

// 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
}

Beim Erstellen generiert das Gradle-Plug-in eine Beibehaltungsregel für den Konstruktor. Im Gegensatz zur vorherigen manuell geschriebenen Regel ist die generierte Regel bedingt: R8 behält den Konstruktor nur bei, wenn loadResourceLocator in der optimierten App verbleibt.

Voraussetzungen

Damit Sie Jetpack Keep verwenden können, muss in Ihrem Projekt das Android-Gradle-Plug-in (AGP) 8.7 oder höher verwendet werden.

Das Gradle-Plug-in unterstützt die folgenden Modultypen:

Modultyp Plug-in, das vom Modul angewendet wird Zusätzliche Einrichtung
Android-App com.android.application Keine
Android-Bibliothek com.android.library Keine
Kotlin Multiplatform-Bibliothek mit einem Android-Ziel com.android.kotlin.multiplatform.library Keine
JVM-Bibliothek (Java oder Kotlin) java-library oder org.jetbrains.kotlin.jvm JAR-Transformation registrieren

Jetpack Keep einrichten

Wenn Sie Jetpack Keep verwenden möchten, fügen Sie jedem Modul, das Reflexion oder JNI verwendet, sowohl die Bibliothek als auch das Gradle-Plug-in hinzu. Die Bibliothek stellt die Annotationen und Lint-Prüfungen bereit und das Plug-in wandelt die Annotationen in Beibehaltungsregeln um.

  1. Fügen Sie in der Datei libs.versions.toml die Bibliothek und das Plug-in hinzu. Die Bibliothek und das Plug-in haben dieselbe Version:

    [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 lädt das Plug-in aus dem Maven-Repository von Google herunter. Achten Sie daher darauf, dass die pluginManagement-Repositories in Ihrer settings.gradle.kts-Datei google() enthalten.

  2. Deklarieren Sie das Plug-in in der build.gradle.kts-Stammdatei zusammen mit dem AGP, ohne es anzuwenden:

    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. Wenden Sie in der Datei build.gradle.kts jedes Moduls, das Reflexion oder JNI verwendet, das Plug-in an und fügen Sie die Bibliotheksabhängigkeit hinzu:

    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. Synchronisieren Sie Ihr Projekt mit Gradle-Dateien.

Für jede Build-Variante eines Android-App-Moduls registriert das Plug-in eine <variant>ExtractKeepRules-Aufgabe, z. B. releaseExtractKeepRules, und übergibt die generierten Regeln für diese Variante an R8. Bei Bibliotheksmodulen verpackt das Plug-in die generierten Regeln als Consumer-Keep-Regeln in der Ausgabe der Bibliothek. Für JVM-Bibliotheksmodule ist eine zusätzliche Einrichtung erforderlich.

Reflexion mit Flusen finden

Jetpack Keep enthält die folgenden Lint-Prüfungen:

ReflectionWithoutKeepAnnotations
Kennzeichnet reflektive Aufrufe wie Class.forName, getDeclaredConstructor, getDeclaredMethod und getDeclaredField, die keine übereinstimmende Keep-Annotation haben.
MissingKeepAnnotationPlugin
Kennzeichnet Module, die von androidx.annotation.keep:keep abhängen, aber das Gradle-Plug-in androidx.annotation.keep nicht anwenden. Ohne das Plug-in werden beim Build keine Beibehaltungsregeln aus den Annotationen generiert.

So verwenden Sie Lint, um ein Android-Modul mit Anmerkungen zu versehen:

  1. Führe Lint für das Modul aus, um reflektierende Aufrufe zu finden, die Annotationen erfordern:

    ./gradlew :app:lint
    
  2. Wenden Sie die Korrekturen an, die Lint automatisch vornehmen kann:

    ./gradlew :app:lintFix
    

    Lint kann automatisch eine Annotation hinzufügen, wenn es ermitteln kann, auf welche Klasse und Mitglieder sich der Code bezieht.

  3. Prüfen Sie jede hinzugefügte Annotation, um sicherzustellen, dass sie mit dem übereinstimmt, was Ihr Code tut. Prüfen Sie beispielsweise, ob die Parametertypen mit dem Konstruktor oder der Methode übereinstimmen, die Ihr Code aufruft.

  4. Beheben Sie die verbleibenden Probleme in Android Studio. Wenn Lint die Zielklasse nicht ermitteln kann, wird die Annotation mit einem TODO-Platzhalter eingefügt, den Sie ausfüllen müssen. Sie können die Annotation auch manuell hinzufügen.

    Die folgende Funktion empfängt beispielsweise den Klassennamen als Parameter, sodass Lint nicht bestimmen kann, welche Klasse erstellt wird. Die Schnellkorrektur fügt eine Annotation mit einem Platzhalter hinzu:

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

    Ersetzen Sie den Platzhalter durch die Klasse, die Ihr Code erstellt. Wenn die Klasse zur Kompilierzeit verfügbar ist, verwenden Sie classConstant anstelle von className. In Kotlin wird Ihr Code erst kompiliert, wenn Sie TODO() ersetzen.

  5. Prüfen Sie, welche Beibehaltungsregeln das Plug-in aus Ihren Annotationen generiert, bevor Sie vorhandene Regeln entfernen. Führe für ein App-Modul die Extraktionsaufgabe für die Release-Variante aus:

    ./gradlew :app:releaseExtractKeepRules
    

    Das Plug-in schreibt die Regeln in app/build/generated/release/androidx.annotation.keep.rules.pro. Informationen zu Bibliotheksmodulen finden Sie unter Generierte Regeln prüfen.

  6. Entfernen Sie die Beibehaltungsregeln, die durch die neuen Annotationen ersetzt werden, aus Ihren Dateien mit Beibehaltungsregeln. Eine Regel kann auch Reflexionen abdecken, die von Lint nicht gekennzeichnet wurden, z. B. Reflexionen in einem anderen Modul oder in einer Bibliothek. Prüfen Sie daher jede Regel, bevor Sie sie entfernen:

    • Achten Sie bei einer paketweiten Regel wie -keep class com.example.** { *; } darauf, dass Annotationen oder andere Regeln alle Reflexionen für die Klassen des Pakets abdecken.
    • Achten Sie bei einer Regel, die bestimmte Klassen oder Mitglieder beibehält, darauf, dass sie genau mit einer der generierten Regeln übereinstimmt oder dass keine andere Reflexion davon abhängt.
  7. Testen Sie Ihren Release-Build.

Code annotieren

Fügen Sie dem Code, der die Reflexion ausführt, eine Jetpack-Keep-Annotation hinzu: die Funktion, den Konstruktor, das Feld oder die Klasse, die den reflektierenden Aufruf enthält. Die Annotation beschreibt das Ziel der Reflexion und die generierte Regel behält dieses Ziel nur bei, wenn R8 den annotierten Code beibehält.

Wählen Sie eine Annotation basierend auf der Funktion Ihres Codes aus:

Annotation Verwenden Sie diese Option, wenn Ihr Code
@UsesReflectionToConstruct Ruft einen Konstruktor über Reflexion auf, z. B. mit getDeclaredConstructor().newInstance().
@UsesReflectionToAccessMethod Ruft eine Methode über Reflexion auf, z. B. mit getMethod oder getDeclaredMethod.
@UsesReflectionToAccessField Liest oder schreibt ein Feld über Reflexion, z. B. mit getField oder getDeclaredField.
@UnconditionallyKeep Wird von außerhalb des Kotlin- oder Java-Codes Ihrer App aufgerufen, z. B. über JNI aus nativem Code. Verwenden Sie diese Annotation nur, wenn Sie die @UsesReflection*-Annotationen, die bedingte Regeln generieren, nicht verwenden können.

Alle @UsesReflection*-Annotationen folgen diesen Konventionen:

  • Identifizieren Sie die Zielklasse mit classConstant oder className. Wenn die Klasse zur Kompilierzeit verfügbar ist, verwenden Sie classConstant. Der Compiler prüft die Referenz und IDE-Refaktorierungen wie das Umbenennen aktualisieren sie. Verwenden Sie className nur, wenn die Klasse zur Kompilierzeit nicht verfügbar ist, z. B. eine Klasse in einer optionalen Abhängigkeit. Diese beiden Parameter schließen sich gegenseitig aus.
  • Spezifischste Klasse auswählen: Die generierten Regeln behalten übereinstimmende Mitglieder in der von Ihnen angegebenen Klasse und in ihren Unterklassen oder Schnittstellenimplementierungen bei. Je allgemeiner die Klasse, desto mehr Code behält R8 bei.
  • Geben Sie Parametertypen an, um eine einzelne Überladung beizubehalten. Wenn Sie parameterTypes oder parameterTypeNames nicht festlegen, werden durch die Regeln alle Konstruktoren oder alle Überladungen der Methode beibehalten. Wenn Sie nur den Konstruktor oder die Methode ohne Argumente beibehalten möchten, übergeben Sie ein leeres Array.
  • Wiederholen Sie die Annotation für mehrere Ziele. Wenn eine Funktion mehr als eine Klasse oder ein Mitglied reflektiert, fügen Sie für jedes Ziel eine Annotation hinzu.

Objekte mit Reflexion erstellen

Verwenden Sie @UsesReflectionToConstruct, wenn Ihr Code eine Klasse durch Reflexion instanziiert. Im folgenden Beispiel instanziiert ein Task Runner Implementierungen einer StartupTask-Schnittstelle nach Name. Da die Annotation die Schnittstelle angibt, behalten die generierten Regeln den Konstruktor ohne Argumente jeder StartupTask-Implementierung bei, solange R8 execute beibehält:

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

Methoden mit Reflexion aufrufen

Verwenden Sie @UsesReflectionToAccessMethod, wenn Ihr Code eine Methode über Reflexion aufruft. Legen Sie für methodName den Namen der Methode fest. Im folgenden Beispiel ruft der Code eine statische Methode in einer optionalen Abhängigkeit auf. Die Klasse ist zur Kompilierzeit nicht verfügbar. Daher wird in der Annotation className anstelle von classConstant verwendet:

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

Zugriff auf Felder mit Reflexion

Verwenden Sie @UsesReflectionToAccessField, wenn Ihr Code ein Feld über Reflexion liest oder schreibt. Legen Sie für fieldName den Namen des Felds fest:

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

Code beibehalten, der von außerhalb Ihrer App aufgerufen wird

Verwende @UnconditionallyKeep für Code, der von außerhalb des Kotlin- oder Java-Codes deiner App aufgerufen wird, z. B. eine Methode, die nativer Code über JNI aufruft. Annotieren Sie das aufgerufene Element, nicht den Code, der es aufruft:

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

Die generierte Regel behält die annotierte Methode und die zugehörige Klasse mit ihren ursprünglichen Namen bei. Andere Klassen, auf die die Signatur der Methode verweist, werden nicht beibehalten. Wenn nativer Code von diesen Klassen abhängt, kommentieren Sie sie ebenfalls oder fügen Sie Beibehaltungsregeln für sie hinzu. Weitere Informationen zu JNI und R8 finden Sie unter Interaktion mit nativem Code über JNI.

Standardmäßig behält @UnconditionallyKeep den Namen des Elements bei. Wenn der externe Aufrufer nicht vom Namen abhängt, legen Sie shouldPreserveName = false fest, damit R8 das Element umbenennen kann.

Generierte Regeln prüfen

In diesem Abschnitt wird erläutert, wie Sie die generierten Regeln für jeden Modultyp prüfen. Bei einem App-Modul übergibt das Plug-in die Regeln direkt an R8. Bei einem Bibliotheksmodul verpackt das Plug-in die Regeln als Consumer-Keep-Regeln in der Ausgabe der Bibliothek. Apps, die von Ihrer Bibliothek abhängen, wenden diese Regeln automatisch an, wenn sie R8 ausführen, auch wenn die Apps Jetpack Keep nicht verwenden. Weitere Informationen zu Aufbewahrungsregeln für Nutzer finden Sie unter Optimierung für Bibliotheksautoren.

Android-Apps

Wenn Sie prüfen möchten, welche Beibehaltungsregeln das Plug-in für ein App-Modul generiert, führen Sie die Extraktionsaufgabe für eine Build-Variante aus:

./gradlew :app:releaseExtractKeepRules

Das Plug-in schreibt die Regeln für jede Variante in app/build/generated/<variant>/androidx.annotation.keep.rules.pro. Ein # context:-Kommentar vor jeder Regel gibt den annotierten Code an, der sie erzeugt hat.

Mit dem R8 Configuration Analyzer können Sie prüfen, wie sich die generierten Regeln zusammen mit Ihren anderen Beibehaltungsregeln auf die Optimierung auswirken.

Android-Bibliotheken

Für Android-Bibliotheksmodule ist keine zusätzliche Einrichtung erforderlich. Wenn Sie das AAR erstellen, fügt das Plug-in die generierten Regeln der proguard.txt-Datei des AAR hinzu, und zwar nach allen Consumer-Keep-Regeln, die das AAR bereits enthält. Das Plug-in registriert dazu für jede Build-Variante eine <variant>KeepRulesTransformAar-Aufgabe.

Wenn Sie die Regeln prüfen möchten, erstellen Sie das AAR, z. B. mit ./gradlew :mylibrary:assembleRelease, und öffnen Sie proguard.txt im generierten AAR. Die generierten Regeln werden in einem Abschnitt mit der folgenden Überschrift angezeigt:

#################################################################
#   Rules generated from androidx.annotation.keep annotations   #
#################################################################

Unter macOS und Linux können Sie die Regeln auch über die Befehlszeile ausgeben:

unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt

Kotlin Multiplatform-Bibliotheken

Kotlin Multiplatform-Bibliotheksmodule mit einem Android-Ziel benötigen ebenfalls keine zusätzliche Einrichtung. Wenn Sie das AAR für das Android-Ziel erstellen, fügt das Plug-in die generierten Regeln der proguard.txt-Datei des AAR auf dieselbe Weise wie für Android-Bibliotheken hinzu. Dazu registriert das Plug-in eine androidMainKeepRulesTransformAar-Aufgabe.

Führen Sie die Aufgabe aus und öffnen Sie proguard.txt im erstellten AAR, um die Regeln zu prüfen. Unter macOS und Linux können Sie die Regeln über die Befehlszeile ausgeben:

./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt

JVM-Bibliotheken

Bevor Sie die Regeln für ein JVM-Bibliotheksmodul prüfen können, müssen Sie das Modul so konfigurieren, dass das Plug-in die generierten Regeln einer Kopie seiner JAR-Datei hinzufügt. Führen Sie dann die registrierte Transformationsaufgabe aus und prüfen Sie die Regeldateien in der erstellten JAR-Datei. Wenn Sie die Aufgabe beispielsweise keepRulesJar genannt haben, können Sie die Regeln unter macOS und Linux mit den folgenden Befehlen ausgeben:

./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
    "META-INF/com.android.tools/*" "META-INF/proguard/*"

JVM-Bibliotheken konfigurieren

Bei JVM-Bibliotheksmodulen, die eine JAR-Datei erzeugen, wird die JAR-Datei vom Plug-in nicht automatisch geändert. Verwenden Sie stattdessen die Funktion annotationKeep.registerJavaArchiveTransform, um eine Aufgabe zu registrieren, die eine Kopie der JAR-Datei mit den generierten Regeln erstellt, und veröffentlichen Sie dann diese JAR-Datei. Fügen Sie der Datei build.gradle.kts des Bibliotheksmoduls Folgendes hinzu:

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

Die Transformation fügt die generierten Regeln dem Zielspeicherort für R8-Regeln der JAR-Datei hinzu: META-INF/com.android.tools/r8/proguard.txt. Wenn die JAR-Datei bereits Regeln in META-INF/com.android.tools/ oder META-INF/proguard/ enthält, werden die generierten Regeln stattdessen an die vorhandene Regeldatei angehängt. Weitere Informationen zu diesen Speicherorten finden Sie unter Verschiedene R8-Versionen unterstützen.

Nachdem Sie die Transformation registriert haben, prüfen Sie die Regeln in der JAR-Datei.

Beispiel: Optionale Add-on-Bibliothek laden

Bibliotheken verwenden häufig Reflection, um optionale Add-ons zu laden. Ein Analyse-SDK kann beispielsweise eine Kernbibliothek und ein Video-Add-on enthalten. Das Add-on ist von der Kernbibliothek abhängig, sodass die Kernbibliothek nicht direkt auf die Klassen des Add-ons verweisen kann. Stattdessen sucht die Kernbibliothek zur Laufzeit nach dem Namen der Klasse des Add-ons und aktiviert die Videoanalyse nur, wenn sie die Klasse findet.

Da die Kernbibliothek die Klasse des Add-ons nur über Reflexion verwendet, kann es sein, dass R8 die Klasse oder ihren Konstruktor aus dem Release-Build einer App entfernt, was dazu führt, dass die Suche fehlschlägt. Der Fehler tritt nur in Release-Builds von Apps auf, die das Add-on enthalten, und ist daher beim Testen schwer zu erkennen.

Ohne Jetpack Keep liefert das Add-on eine Consumer-Keep-Regel für seine Klasse, wie unter Reflection zur Unterstützung optionaler Abhängigkeiten gezeigt:

# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

Diese Regel befindet sich in einer anderen Bibliothek als die Suchanfrage, die sie benötigt. Daher können die beiden nicht mehr synchron sein. Mit Jetpack Keep annotieren Sie stattdessen die Suche in der Kernbibliothek:

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

Die Annotation folgt diesen Richtlinien:

  • Annotieren Sie den Code, der die Reflexion ausführt. Die Annotation befindet sich auf initialize, das Class.forName aufruft, nicht auf der Klasse des Add-ons.
  • Wählen Sie die Annotation basierend auf der Funktion des Codes aus. Bei der Suche wird eine Instanz der Klasse erstellt, sodass im Code @UsesReflectionToConstruct verwendet wird.
  • Verwenden Sie className für eine Klasse, auf die Sie nicht verweisen können. Die Klasse des Add-ons befindet sich nicht im Kompilierungs-Klassenpfad der Hauptbibliothek, daher kann die Annotation nicht classConstant verwenden.
  • Behalten Sie nur den Konstruktor bei, der vom Code aufgerufen wird. Bei der Suche wird der Konstruktor ohne Argumente aufgerufen, sodass parameterTypes ein leeres Array ist.

Wenn Sie das AAR der Kernbibliothek erstellen, fügt das Plug-in eine Regel hinzu, die VideoEventTracker und den Konstruktor ohne Argumente beibehält, wenn R8 initialize beibehält. Apps, die das SDK verwenden, wenden diese Regel automatisch an, auch wenn sie Jetpack Keep nicht verwenden. Das Add-on benötigt also keine eigene Regel. Im Vergleich zur Regel des Add-ons bietet dieser Ansatz folgende Vorteile:

  • Die Regel bleibt neben der Suche. Die Kernbibliothek enthält sowohl die Suchfunktion als auch die Regel. Sie können sie also zusammen aktualisieren, anstatt eine Änderung in zwei Bibliotheken zu koordinieren.
  • R8 kann weiterhin nicht verwendeten Code entfernen. Wenn eine App das Add-on enthält, aber nie initialize aufruft, gilt die generierte Regel nicht und R8 kann VideoEventTracker entfernen. Die Regel des Add-ons sorgt dafür, dass es in jeder App, die das Add-on enthält, beibehalten wird.
  • Lint erkennt nicht annotierte Suchvorgänge. Wenn die Hauptbibliothek später eine weitere reflektierende Suche ohne Annotation hinzufügt, wird sie von Lint gekennzeichnet, damit Sie sie vor der Veröffentlichung der Bibliothek beheben können.

Prüfen Sie, ob die generierte Regel in der proguard.txt-Datei der AAR-Datei enthalten ist, wie unter Android-Bibliotheken beschrieben. Testen Sie dann einen Release-Build einer App, die das Add-on enthält, da eine fehlende Regel dort die Videoanalyse beeinträchtigen würde.

Einschränkungen

Jetpack Keep erkennt noch keine Reflexion, die Klassen und Mitglieder anhand ihrer Annotationen findet, und generiert auch keine Keep-Regeln dafür. Diese Funktion ist geplant.

Bis dahin können Sie Aufbewahrungsregeln für diese Art von Reflexion verwenden. Mit der folgenden Regel werden beispielsweise alle Mitglieder, die mit @SomeAnnotation annotiert sind, in den Klassen beibehalten, die R8 beibehält:

-keepclassmembers class * {
    @com.example.annotation.SomeAnnotation <methods>;
    @com.example.annotation.SomeAnnotation <fields>;
}

Wenn Jetpack Keep für Ihren Anwendungsfall nicht funktioniert oder Sie anderes Feedback haben, melden Sie ein Problem.

Zusätzliche Ressourcen