Genera regole di conservazione dalle annotazioni

Jetpack Keep ti consente di dichiarare l'utilizzo di reflection e Java Native Interface (JNI) con le annotazioni direttamente nel codice sorgente Kotlin o Java, anziché in file di regole di conservazione separati. Il plug-in Gradle di Jetpack Keep legge queste annotazioni in fase di compilazione e genera le regole di conservazione necessarie per R8.

Jetpack Keep aiuta l'ottimizzazione R8 a rimanere efficace man mano che la codebase cambia:

  • Le regole di conservazione rimangono accanto al codice che le richiede. Quando rinomini o elimini il codice che utilizza la reflection, aggiorni l'annotazione nello stesso punto. Non esiste un file di regole separato da mantenere sincronizzato.

  • Le regole generate sono condizionali. R8 mantiene il target di una chiamata reflection solo se mantiene anche il codice che effettua la chiamata. Se il codice chiamante non viene utilizzato e R8 lo rimuove (spesso è il caso delle funzionalità della libreria facoltative), R8 può anche rimuovere il target, in modo che le regole delle funzionalità non utilizzate non aggiungano bloat.

  • Lint trova la reflection per te. La libreria include controlli lint che segnalano le chiamate reflection non annotate e che possono aggiungere automaticamente molte delle annotazioni.

Come funziona Jetpack Keep

Considera una funzione che crea un'istanza di una classe per nome. Se R8 non riesce a capire che il codice utilizza la classe, rimuove il costruttore della classe e l'app si arresta in modo anomalo in fase di runtime. Per evitare l'arresto anomalo, puoi aggiungere una regola come la seguente al file delle regole di conservazione:

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

Con Jetpack Keep, annoti invece la funzione che esegue la reflection:

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

In fase di compilazione, il plug-in Gradle genera una regola di conservazione per il costruttore. A differenza della regola scritta manualmente precedente, la regola generata è condizionale: R8 mantiene il costruttore solo se loadResourceLocator rimane nell'app ottimizzata.

Requisiti

Per utilizzare Jetpack Keep, il progetto deve avere il plug-in Android per Gradle (AGP) 8.7 o versioni successive.

Il plug-in Gradle supporta i seguenti tipi di moduli:

Module type Plugin that the module applies Additional setup
App per Android com.android.application Nessuno
Libreria Android com.android.library Nessuno
Libreria multipiattaforma Kotlin con un target Android com.android.kotlin.multiplatform.library Nessuno
Libreria JVM (Java o Kotlin) java-library or org.jetbrains.kotlin.jvm Registra una trasformazione JAR

Configurare Jetpack Keep

Per utilizzare Jetpack Keep, aggiungi sia la libreria sia il plug-in Gradle a ogni modulo che utilizza la reflection o JNI. La libreria fornisce le annotazioni e i controlli lint, mentre il plug-in trasforma le annotazioni in regole di conservazione.

  1. Nel file libs.versions.toml, aggiungi la libreria e il plug-in. La libreria e il plug-in condividono la stessa versione:

    [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 scarica il plug-in dal repository Maven di Google, quindi assicurati che i repository pluginManagement nel file settings.gradle.kts includano google().

  2. Nel file build.gradle.kts principale, dichiara il plug-in insieme ad AGP senza applicarlo:

    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. Nel file build.gradle.kts di ogni modulo che utilizza la reflection o JNI, applica il plug-in e aggiungi la dipendenza della libreria:

    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. Sincronizza il progetto con i file Gradle.

Per ogni variante di compilazione di un modulo dell'app per Android, il plug-in registra un'attività <variant>ExtractKeepRules come releaseExtractKeepRules, e passa le regole generate a R8 per quella variante di compilazione. Per i moduli della libreria, il plug-in pacchettizza le regole generate come regole di conservazione del consumatore nell'output della libreria. I moduli della libreria JVM richiedono una configurazione aggiuntiva.

Trovare la reflection con lint

Jetpack Keep include i seguenti controlli lint:

ReflectionWithoutKeepAnnotations
Segnala le chiamate reflection, come Class.forName, getDeclaredConstructor, getDeclaredMethod e getDeclaredField, che non hanno un'annotazione di conservazione corrispondente.
MissingKeepAnnotationPlugin
Segnala i moduli che dipendono da androidx.annotation.keep:keep ma non applicano il plug-in Gradle androidx.annotation.keep. Senza il plug-in, la build non genera regole di conservazione dalle annotazioni.

Per utilizzare lint per annotare un modulo Android:

  1. Esegui lint sul modulo per trovare le chiamate reflection che richiedono annotazioni:

    ./gradlew :app:lint
    
  2. Applica le correzioni che lint può apportare automaticamente:

    ./gradlew :app:lintFix
    

    Lint può aggiungere automaticamente un'annotazione quando riesce a determinare la classe e i membri su cui il codice esegue la reflection.

  3. Esamina ogni annotazione aggiunta per verificare che corrisponda a ciò che fa il codice. Ad esempio, verifica che i tipi di parametri corrispondano al costruttore o al metodo chiamato dal codice.

  4. Risolvi i risultati rimanenti in Android Studio. Quando lint non riesce a determinare la classe di destinazione, la correzione rapida inserisce l'annotazione con un segnaposto TODO da completare. Puoi anche aggiungere l'annotazione manualmente.

    Ad esempio, la seguente funzione riceve il nome della classe come parametro, quindi lint non riesce a determinare la classe che costruisce. La correzione rapida aggiunge un'annotazione con un segnaposto:

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

    Sostituisci il segnaposto con la classe che il codice costruisce. Se la classe è disponibile in fase di compilazione, utilizza classConstant anziché className. In Kotlin, il codice non viene compilato finché non sostituisci TODO().

  5. Controlla le regole di conservazione generate dal plug-in dalle annotazioni prima di rimuovere le regole esistenti. Per un modulo dell'app, esegui l'attività di estrazione per la variante di release:

    ./gradlew :app:releaseExtractKeepRules
    

    Il plug-in scrive le regole in app/build/generated/release/androidx.annotation.keep.rules.pro. Per i moduli della libreria, vedi Verificare le regole generate.

  6. Rimuovi le regole di conservazione sostituite dalle nuove annotazioni dai file delle regole di conservazione. Una regola potrebbe anche coprire la reflection che lint non ha segnalato, ad esempio la reflection in un altro modulo o in una libreria, quindi controlla ogni regola prima di rimuoverla:

    • Per una regola a livello di pacchetto, ad esempio -keep class com.example.** { *; }, assicurati che le annotazioni o altre regole coprano tutte le reflection sulle classi del pacchetto.
    • Per una regola che mantiene classi o membri specifici, assicurati che corrisponda strettamente a una delle regole generate o che non dipenda da altre reflection.
  7. Testa la build di release.

Annotare il codice

Aggiungi un'annotazione Jetpack Keep al codice che esegue la reflection: la funzione, il costruttore, il campo o la classe che contiene la chiamata reflection. L'annotazione descrive il target della reflection e la regola generata mantiene il target solo se R8 mantiene il codice annotato.

Scegli un'annotazione in base a ciò che fa il codice:

Annotation Use when your code
@UsesReflectionToConstruct Chiama un costruttore tramite reflection, ad esempio con getDeclaredConstructor().newInstance().
@UsesReflectionToAccessMethod Chiama un metodo tramite reflection, ad esempio con getMethod o getDeclaredMethod.
@UsesReflectionToAccessField Legge o scrive un campo tramite reflection, ad esempio con getField o getDeclaredField.
@UnconditionallyKeep Viene chiamato dall'esterno del codice Kotlin o Java dell'app, ad esempio dal codice nativo tramite JNI. Utilizza questa annotazione solo quando non puoi utilizzare le @UsesReflection* annotazioni, che generano regole condizionali.

Tutte le annotazioni @UsesReflection* seguono queste convenzioni:

  • Identifica la classe di destinazione con classConstant o className. Se la classe è disponibile in fase di compilazione, utilizza classConstant. Il compilatore controlla il riferimento e i refactoring dell'IDE, come la ridenominazione, lo aggiornano. Utilizza className solo quando la classe non è disponibile in fase di compilazione, ad esempio una classe in una dipendenza facoltativa. Questi due parametri si escludono a vicenda.
  • Scegli come target la classe più specifica. Le regole generate mantengono i membri corrispondenti nella classe specificata e nelle relative sottoclassi o implementazioni di interfacce. Più generale è la classe, più codice R8 mantiene.
  • Specifica i tipi di parametri per mantenere un singolo overload. Se non imposti parameterTypes o parameterTypeNames, le regole mantengono tutti i costruttori o tutti gli overload del metodo. Per mantenere solo il costruttore o il metodo senza argomenti, passa un array vuoto.
  • Ripeti l'annotazione per più target. Se una funzione esegue la reflection su più classi o membri, aggiungi un'annotazione per ogni target.

Costruire oggetti con la reflection

Utilizza @UsesReflectionToConstruct quando il codice crea un'istanza di una classe tramite reflection. Nell'esempio seguente, un esecutore di attività crea un'istanza delle implementazioni di un'interfaccia StartupTask per nome. Poiché l'annotazione specifica l'interfaccia, le regole generate mantengono il costruttore senza argomenti di ogni implementazione StartupTask, a condizione che R8 mantenga 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()
    }
}

Chiamare i metodi con la reflection

Utilizza @UsesReflectionToAccessMethod quando il codice chiama un metodo tramite reflection. Imposta methodName sul nome del metodo. Nell'esempio seguente, il codice chiama un metodo statico in una dipendenza facoltativa. La classe non è disponibile in fase di compilazione, quindi l'annotazione utilizza className anziché 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)
    }
}

Accedere ai campi con la reflection

Utilizza @UsesReflectionToAccessField quando il codice legge o scrive un campo tramite reflection. Imposta fieldName sul nome del campo:

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

Mantenere il codice chiamato dall'esterno dell'app

Utilizza @UnconditionallyKeep per il codice chiamato dall'esterno del codice Kotlin o Java dell'app, ad esempio un metodo chiamato dal codice nativo tramite JNI. Annota l'elemento chiamato, non il codice che lo chiama:

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

La regola generata mantiene il metodo annotato e la relativa classe contenitore, con i nomi originali. Non mantiene altre classi a cui fa riferimento la firma del metodo. Se il codice nativo dipende da queste classi, annotale anche tu o aggiungi regole di conservazione per queste. Per saperne di più su JNI e R8, vedi Interazione con il codice nativo tramite JNI.

Per impostazione predefinita, @UnconditionallyKeep conserva il nome dell'elemento. Se il chiamante esterno non dipende dal nome, imposta shouldPreserveName = false in modo che R8 possa rinominare l'elemento.

Verificare le regole generate

Questa sezione spiega come controllare le regole generate per ogni tipo di modulo. Per un modulo dell'app, il plug-in passa le regole direttamente a R8. Per un modulo della libreria, il plug-in pacchettizza le regole come regole di conservazione del consumatore nell'output della libreria. Le app che dipendono dalla tua libreria applicano automaticamente queste regole quando eseguono R8, anche se non utilizzano Jetpack Keep. Per saperne di più sulle regole di conservazione del consumatore, vedi Ottimizzazione per gli autori di librerie.

App per Android

Per controllare le regole di conservazione generate dal plug-in per un modulo dell'app, esegui l'attività di estrazione per una variante di compilazione:

./gradlew :app:releaseExtractKeepRules

Il plug-in scrive le regole per ogni variante in app/build/generated/<variant>/androidx.annotation.keep.rules.pro. Un commento # context: prima di ogni regola identifica il codice annotato che l'ha generata.

Per verificare in che modo le regole generate influiscono sull'ottimizzazione insieme alle altre regole di conservazione, utilizza lo strumento di analisi della configurazione R8.

Librerie Android

I moduli della libreria Android non richiedono alcuna configurazione aggiuntiva. Quando crei l'AAR, il plug-in aggiunge le regole generate al file proguard.txt dell'AAR, dopo le eventuali regole di conservazione del consumatore già contenute nell'AAR. Il plug-in registra un'attività <variant>KeepRulesTransformAar per ogni variante di compilazione.

Per verificare le regole, crea l'AAR, ad esempio con ./gradlew :mylibrary:assembleRelease, e apri proguard.txt nell'AAR generato. Le regole generate vengono visualizzate in una sezione con la seguente intestazione:

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

Su macOS e Linux, puoi anche stampare le regole dalla riga di comando:

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

Librerie multipiattaforma Kotlin

Anche i moduli della libreria multipiattaforma Kotlin con un target Android non richiedono alcuna configurazione aggiuntiva. Quando crei l'AAR per il target Android, il plug-in aggiunge le regole generate al file proguard.txt dell'AAR nello stesso modo delle librerie Android. Il plug-in registra un'attività androidMainKeepRulesTransformAar per eseguire questa operazione.

Per verificare le regole, esegui l'attività e apri proguard.txt nell'AAR che crea. Su macOS e Linux, puoi stampare le regole dalla riga di comando:

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

Librerie JVM

Prima di poter verificare le regole per un modulo della libreria JVM, configura il modulo in modo che il plug-in aggiunga le regole generate a una copia del relativo JAR. Poi esegui l'attività di trasformazione registrata e controlla i file delle regole nel JAR che crea. Ad esempio, se hai chiamato l'attività keepRulesJar, puoi stampare le regole su macOS e Linux con i seguenti comandi:

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

Configurare le librerie JVM

Per i moduli della libreria JVM che producono un JAR, il plug-in non modifica automaticamente il JAR. Utilizza invece la funzione annotationKeep.registerJavaArchiveTransform per registrare un'attività che crea una copia del JAR con le regole generate e poi pubblica il JAR. Aggiungi quanto segue al file build.gradle.kts del modulo della libreria:

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

La trasformazione aggiunge le regole generate alla posizione delle regole R8 di destinazione del JAR, META-INF/com.android.tools/r8/proguard.txt. Se il JAR contiene già regole in META-INF/com.android.tools/ o META-INF/proguard/, la trasformazione aggiunge le regole generate al file delle regole esistente. Per saperne di più su queste posizioni, vedi Supportare diverse versioni di R8.

Dopo aver registrato la trasformazione, verifica le regole nel JAR.

Esempio: caricare una libreria di componenti aggiuntivi facoltativa

Le librerie spesso utilizzano la reflection per caricare componenti aggiuntivi facoltativi. Ad esempio, un SDK Analytics potrebbe includere una libreria principale e un componente aggiuntivo video. Il componente aggiuntivo dipende dalla libreria principale, quindi la libreria principale non può fare riferimento direttamente alle classi del componente aggiuntivo. La libreria principale cerca invece la classe del componente aggiuntivo per nome in fase di runtime e attiva l'analisi video solo se trova la classe.

Poiché la libreria principale utilizza la classe del componente aggiuntivo solo tramite reflection, R8 potrebbe rimuovere la classe o il relativo costruttore dalla build di release di un'app, causando un errore nella ricerca. L'errore si verifica solo nelle build di release delle app che includono il componente aggiuntivo, quindi è difficile rilevarlo durante i test.

Senza Jetpack Keep, il componente aggiuntivo include una regola di conservazione del consumatore per la relativa classe, come mostrato in Reflection per supportare le dipendenze facoltative:

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

Questa regola si trova in una libreria diversa dalla ricerca che la richiede, quindi le due possono non essere sincronizzate. Con Jetpack Keep, annoti invece la ricerca nella libreria principale:

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

L'annotazione segue queste linee guida:

  • Annota il codice che esegue la reflection. L'annotazione si trova su initialize, che chiama Class.forName, non sulla classe del componente aggiuntivo.
  • Scegli l'annotazione in base a ciò che fa il codice. La ricerca crea un'istanza della classe, quindi il codice utilizza @UsesReflectionToConstruct.
  • Utilizza className per una classe a cui non puoi fare riferimento. La classe del componente aggiuntivo non si trova nel classpath di compilazione della libreria principale, quindi l'annotazione non può utilizzare classConstant.
  • Mantieni solo il costruttore chiamato dal codice. La ricerca chiama il costruttore senza argomenti, quindi parameterTypes è un array vuoto.

Quando crei l'AAR della libreria principale, il plug-in aggiunge una regola che mantiene VideoEventTracker e il relativo costruttore senza argomenti ogni volta che R8 mantiene initialize. Le app che utilizzano l'SDK applicano automaticamente questa regola, anche se non utilizzano Jetpack Keep, quindi il componente aggiuntivo non ha bisogno di una regola propria. Rispetto alla regola del componente aggiuntivo, questo approccio presenta i seguenti vantaggi:

  • La regola rimane accanto alla ricerca. La libreria principale possiede sia la ricerca sia la regola, quindi le aggiorni insieme anziché coordinare una modifica in due librerie.
  • R8 può comunque rimuovere il codice non utilizzato. Se un'app include il componente aggiuntivo ma non chiama mai initialize, la regola generata non viene applicata e R8 può rimuovere VideoEventTracker. La regola del componente aggiuntivo lo mantiene in ogni app che include il componente aggiuntivo.
  • Lint rileva le ricerche non annotate. Se in un secondo momento la libreria principale aggiunge un'altra ricerca reflection senza un'annotazione, lint la segnala, in modo che tu possa correggerla prima di rilasciare la libreria.

Per verificare il risultato, controlla che la regola generata venga visualizzata nel file dell'AAR's proguard.txt, come descritto in Librerie Android. Poi testa una build di release di un'app che include il componente aggiuntivo, perché è qui che una regola mancante interromperebbe l'analisi video.

Risorse aggiuntive