Jetpack Keep vous permet de déclarer l'utilisation de la réflexion et de l'interface JNI (Java Native Interface) avec des annotations directement dans votre code source Kotlin ou Java, au lieu de le faire dans des fichiers de règles de conservation distincts. Le plug-in Gradle Jetpack Keep lit ces annotations au moment de la compilation et génère les règles de conservation dont R8 a besoin.
Jetpack Keep permet à l'optimisation R8 de rester efficace à mesure que votre base de code évolue :
Les règles de conservation restent à côté du code qui en a besoin. Lorsque vous renommez ou supprimez du code qui utilise la réflexion, vous mettez à jour l'annotation au même endroit. Il n'y a pas de fichier de règles distinct à synchroniser.
Les règles générées sont conditionnelles. R8 ne conserve la cible d'un appel réflexif que s'il conserve également le code qui effectue l'appel. Si le code appelant n'est pas utilisé et que R8 le supprime (ce qui est souvent le cas dans les fonctionnalités de bibliothèque facultatives), R8 peut également supprimer la cible. Ainsi, les règles des fonctionnalités inutilisées n'ajoutent pas de surcharge.
Lint trouve la réflexion pour vous. La bibliothèque inclut des vérifications lint qui signalent les appels réflexifs qui ne sont pas annotés et qui peuvent ajouter automatiquement de nombreuses annotations.
Fonctionnement de Jetpack Keep
Prenons l'exemple d'une fonction qui instancie une classe par son nom. Si R8 ne peut pas déterminer que votre code utilise la classe, il supprime le constructeur de la classe et l'application plante au moment de l'exécution. Pour éviter ce plantage, vous pouvez ajouter une règle comme celle-ci à votre fichier de règles de conservation :
# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
<init>();
}
Avec Jetpack Keep, vous annotez plutôt la fonction qui effectue la réflexion :
// 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
}
Au moment de la compilation, le plug-in Gradle génère une règle de conservation pour le constructeur.
Contrairement à la règle écrite manuellement précédente, la règle générée est conditionnelle : R8 ne conserve le constructeur que si loadResourceLocator reste dans l'application optimisée.
Conditions requises
Pour utiliser Jetpack Keep, votre projet doit disposer du plug-in Android Gradle (AGP) 8.7 ou version ultérieure.
Le plug-in Gradle est compatible avec les types de modules suivants :
| Module type | Plugin that the module applies | Configuration supplémentaire |
|---|---|---|
| Application Android | com.android.application |
Aucun |
| Bibliothèque Android | com.android.library |
Aucun |
| Bibliothèque multiplateforme Kotlin avec une cible Android | com.android.kotlin.multiplatform.library |
Aucun |
| Bibliothèque JVM (Java ou Kotlin) | java-library ou org.jetbrains.kotlin.jvm |
Enregistrer une transformation JAR |
Configurer Jetpack Keep
Pour utiliser Jetpack Keep, ajoutez la bibliothèque et le plug-in Gradle à chaque module qui utilise la réflexion ou JNI. La bibliothèque fournit les annotations et les vérifications lint, et le plug-in transforme les annotations en règles de conservation.
Dans votre fichier
libs.versions.toml, ajoutez la bibliothèque et le plug-in. La bibliothèque et le plug-in partagent la même 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 télécharge le plug-in à partir du dépôt Maven de Google. Assurez-vous donc que les dépôts
pluginManagementde votre fichiersettings.gradle.ktsincluentgoogle().Dans votre fichier
build.gradle.ktsracine, déclarez le plug-in avec AGP sans l'appliquer :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 }Dans le fichier
build.gradle.ktsde chaque module qui utilise la réflexion ou JNI, appliquez le plug-in et ajoutez la dépendance de la bibliothèque :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) }Synchronisez votre projet avec les fichiers Gradle.
Pour chaque variante de compilation d'un module d'application Android, le plug-in enregistre une
<variant>ExtractKeepRules tâche, telle que releaseExtractKeepRules, et transmet
les règles générées à R8 pour cette variante de compilation. Pour les modules de bibliothèque, le plug-in regroupe les règles générées en tant que règles de conservation du consommateur dans la sortie de la bibliothèque.
Les modules de bibliothèque JVM nécessitent une configuration supplémentaire.
Trouver la réflexion avec lint
Jetpack Keep inclut les vérifications lint suivantes :
ReflectionWithoutKeepAnnotations- Signale les appels réflexifs, tels que
Class.forName,getDeclaredConstructor,getDeclaredMethodetgetDeclaredField, qui ne disposent pas d'annotation de conservation correspondante. MissingKeepAnnotationPlugin- Signale les modules qui dépendent de
androidx.annotation.keep:keepmais n'appliquent pas le plug-in Gradleandroidx.annotation.keep. Sans le plug-in, votre build ne génère pas de règles de conservation à partir des annotations.
Pour utiliser lint afin d'annoter un module Android, procédez comme suit :
Exécutez lint sur le module pour trouver les appels réflexifs qui nécessitent des annotations :
./gradlew :app:lintAppliquez les corrections que lint peut effectuer automatiquement :
./gradlew :app:lintFixLint peut ajouter une annotation automatiquement lorsqu'il peut déterminer la classe et les membres sur lesquels le code se reflète.
Vérifiez chaque annotation ajoutée pour vous assurer qu'elle correspond à ce que fait votre code. Par exemple, vérifiez que les types de paramètres correspondent au constructeur ou à la méthode que votre code appelle.
Résolvez les résultats restants dans Android Studio. Lorsque lint ne peut pas déterminer la classe cible, la correction rapide insère l'annotation avec un espace réservé
TODOque vous devez compléter. Vous pouvez également ajouter l'annotation manuellement.Par exemple, la fonction suivante reçoit le nom de la classe en tant que paramètre. Lint ne peut donc pas déterminer la classe qu'elle construit. La correction rapide ajoute une annotation avec un espace réservé :
@UsesReflectionToConstruct( className = TODO(), parameterTypes = [] ) fun createInstance(className: String): Any = Class.forName(className).getDeclaredConstructor().newInstance()Remplacez l'espace réservé par la classe que votre code construit. Si la classe est disponible au moment de la compilation, utilisez
classConstantau lieu declassName. En Kotlin, votre code n'est compilé que lorsque vous remplacezTODO().Vérifiez les règles de conservation que le plug-in génère à partir de vos annotations avant de supprimer les règles existantes. Pour un module d'application, exécutez la tâche d'extraction pour la variante de version :
./gradlew :app:releaseExtractKeepRulesLe plug-in écrit les règles dans
app/build/generated/release/androidx.annotation.keep.rules.pro. Pour les modules de bibliothèque, consultez Vérifier les règles générées.Supprimez les règles de conservation que les nouvelles annotations remplacent dans vos fichiers de règles de conservation. Une règle peut également couvrir une réflexion que lint n'a pas signalée, par exemple une réflexion dans un autre module ou dans une bibliothèque. Vérifiez donc chaque règle avant de la supprimer :
- Pour une règle à l'échelle du package, telle que
-keep class com.example.** { *; }, assurez-vous que les annotations ou d'autres règles couvrent toutes les réflexions sur les classes du package. - Pour une règle qui conserve des classes ou des membres spécifiques, assurez-vous qu'elle correspond étroitement à l'une des règles générées ou qu'aucune autre réflexion n'en dépend.
- Pour une règle à l'échelle du package, telle que
Testez votre build.
Annoter votre code
Ajoutez une annotation Jetpack Keep au code qui effectue la réflexion : la fonction, le constructeur, le champ ou la classe qui contient l'appel réflexif. L'annotation décrit la cible de la réflexion, et la règle générée ne conserve cette cible que si R8 conserve le code annoté.
Choisissez une annotation en fonction de ce que fait votre code :
| Annotation | Cas d'utilisation |
|---|---|
@UsesReflectionToConstruct |
Appelle un constructeur par réflexion, par exemple avec
getDeclaredConstructor().newInstance(). |
@UsesReflectionToAccessMethod |
Appelle une méthode par réflexion, par exemple avec
getMethod ou getDeclaredMethod. |
@UsesReflectionToAccessField |
Lit ou écrit un champ par réflexion, par exemple avec
getField ou getDeclaredField. |
@UnconditionallyKeep |
Est appelé depuis l'extérieur du code Kotlin ou Java de votre application, par exemple
à partir de code natif via JNI. N'utilisez cette annotation que lorsque vous ne pouvez pas
utiliser les @UsesReflection* annotations, qui génèrent
des règles conditionnelles. |
Toutes les annotations @UsesReflection* suivent ces conventions :
- Identifiez la classe cible avec
classConstantouclassName. Si la classe est disponible au moment de la compilation, utilisezclassConstant. Le compilateur vérifie la référence, et les refactorisations de l'IDE, telles que le renommage, la mettent à jour. N'utilisezclassNameque lorsque la classe n'est pas disponible au moment de la compilation, par exemple une classe dans une dépendance facultative. Ces deux paramètres s'excluent mutuellement. - Ciblez la classe la plus spécifique. Les règles générées conservent les membres correspondants dans la classe que vous spécifiez, ainsi que dans ses sous-classes ou implémentations d'interface. Plus la classe est générale, plus R8 conserve de code.
- Spécifiez les types de paramètres pour conserver une seule surcharge. Si vous ne définissez pas
parameterTypesouparameterTypeNames, les règles conservent tous les constructeurs ou toutes les surcharges de la méthode. Pour ne conserver que le constructeur ou la méthode sans argument, transmettez un tableau vide. - Répétez l'annotation pour plusieurs cibles. Si une fonction se reflète sur plusieurs classes ou membres, ajoutez une annotation pour chaque cible.
Construire des objets avec la réflexion
Utilisez @UsesReflectionToConstruct lorsque votre code instancie une classe par réflexion. Dans l'exemple suivant, un exécuteur de tâches instancie des implémentations d'une interface StartupTask par son nom. Étant donné que l'annotation spécifie l'interface, les règles générées conservent le constructeur sans argument de chaque implémentation StartupTask, tant que R8 conserve 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()
}
}
Appeler des méthodes avec la réflexion
Utilisez @UsesReflectionToAccessMethod lorsque votre code appelle une méthode par réflexion. Définissez methodName sur le nom de la méthode. Dans l'exemple suivant, le code appelle une méthode statique dans une dépendance facultative. La classe n'est pas disponible au moment de la compilation. L'annotation utilise donc className au lieu de 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)
}
}
Accéder aux champs avec la réflexion
Utilisez @UsesReflectionToAccessField lorsque votre code lit ou écrit un champ par réflexion. Définissez fieldName sur le nom du champ :
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)
}
}
Conserver le code appelé depuis l'extérieur de votre application
Utilisez @UnconditionallyKeep pour le code appelé depuis l'extérieur du code Kotlin ou Java de votre application, par exemple une méthode que le code natif appelle via JNI. Annotez l'élément appelé, et non le code qui l'appelle :
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 règle générée conserve la méthode annotée et sa classe conteneur, avec leurs noms d'origine. Elle ne conserve pas les autres classes auxquelles la signature de la méthode fait référence. Si le code natif dépend de ces classes, annotez-les également ou ajoutez des règles de conservation pour elles. Pour en savoir plus sur JNI et R8, consultez Interaction avec le code natif via JNI.
Par défaut, @UnconditionallyKeep conserve le nom de l'élément. Si l'appelant externe ne dépend pas du nom, définissez shouldPreserveName = false pour que R8 puisse renommer l'élément.
Vérifier les règles générées
Cette section explique comment vérifier les règles générées pour chaque type de module. Pour un module d'application, le plug-in transmet directement les règles à R8. Pour un module de bibliothèque, le plug-in regroupe les règles en tant que règles de conservation du consommateur dans la sortie de la bibliothèque. Les applications qui dépendent de votre bibliothèque appliquent automatiquement ces règles lorsqu'elles exécutent R8, même si elles n'utilisent pas Jetpack Keep. Pour en savoir plus sur les règles de conservation du consommateur, consultez Optimisation pour les auteurs de bibliothèques.
Applications Android
Pour vérifier les règles de conservation que le plug-in génère pour un module d'application, exécutez la tâche d'extraction pour une variante de compilation :
./gradlew :app:releaseExtractKeepRules
Le plug-in écrit les règles pour chaque variante dans
app/build/generated/<variant>/androidx.annotation.keep.rules.pro. Un commentaire # context: avant chaque règle identifie le code annoté qui l'a produite.
Pour vérifier l'impact des règles générées sur l'optimisation avec vos autres règles de conservation, utilisez l'analyseur de configuration R8.
Bibliothèques Android
Les modules de bibliothèque Android ne nécessitent aucune configuration supplémentaire. Lorsque vous compilez l'AAR, le plug-in ajoute les règles générées au fichier proguard.txt de l'AAR, après toutes les règles de conservation du consommateur que l'AAR contient déjà. Pour ce faire, le plug-in enregistre une tâche
<variant>KeepRulesTransformAar pour chaque variante de compilation.
Pour vérifier les règles, compilez l'AAR, par exemple avec ./gradlew
:mylibrary:assembleRelease, puis ouvrez proguard.txt dans l'AAR généré. Les règles générées apparaissent dans une section avec l'en-tête suivant :
#################################################################
# Rules generated from androidx.annotation.keep annotations #
#################################################################
Sous macOS et Linux, vous pouvez également imprimer les règles à partir de la ligne de commande :
unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt
Bibliothèques multiplateformes Kotlin
Les modules de bibliothèque multiplateforme Kotlin avec une cible Android ne nécessitent aucune configuration supplémentaire. Lorsque vous compilez l'AAR pour la cible Android, le
plug-in ajoute les règles générées au fichier proguard.txt de l'AAR de la même
manière que pour les bibliothèques Android. Pour ce faire, le plug-in enregistre une tâche androidMainKeepRulesTransformAar.
Pour vérifier les règles, exécutez cette tâche et ouvrez proguard.txt dans l'AAR qu'elle crée. Sous macOS et Linux, vous pouvez imprimer les règles à partir de la ligne de commande :
./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt
Bibliothèques JVM
Avant de pouvoir vérifier les règles d'un module de bibliothèque JVM, configurez le
module de sorte que le plug-in ajoute les règles générées à une copie
de son fichier JAR. Exécutez ensuite la tâche de transformation que vous avez enregistrée et vérifiez les fichiers de règles dans le fichier JAR qu'elle crée. Par exemple, si vous avez nommé la tâche keepRulesJar, vous pouvez imprimer les règles sous macOS et Linux avec les commandes suivantes :
./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
"META-INF/com.android.tools/*" "META-INF/proguard/*"
Configurer les bibliothèques JVM
Pour les modules de bibliothèque JVM qui produisent un fichier JAR, le plug-in ne modifie pas automatiquement le fichier JAR. Utilisez plutôt la fonction annotationKeep.registerJavaArchiveTransform pour enregistrer une tâche qui crée une copie du fichier JAR avec les règles générées, puis publiez ce fichier JAR. Ajoutez le code suivant au fichier build.gradle.kts du module de bibliothèque :
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 transformation ajoute les règles générées à l'emplacement des règles R8 cibles du fichier JAR, META-INF/com.android.tools/r8/proguard.txt. Si le fichier JAR contient déjà des règles dans META-INF/com.android.tools/ ou META-INF/proguard/, la transformation ajoute les règles générées au fichier de règles existant. Pour en savoir plus
sur ces emplacements, consultez Compatibilité avec différentes versions de R8.
Une fois la transformation enregistrée, vérifiez les règles dans le fichier JAR.
Exemple : Charger une bibliothèque d'extensions facultative
Les bibliothèques utilisent souvent la réflexion pour charger des extensions facultatives. Par exemple, un SDK d'analyse peut fournir une bibliothèque de base et une extension vidéo. L'extension dépend de la bibliothèque de base. La bibliothèque de base ne peut donc pas faire référence directement aux classes de l'extension. Au lieu de cela, la bibliothèque de base recherche la classe de l'extension par son nom au moment de l'exécution et n'active l'analyse vidéo que si elle trouve la classe.
Étant donné que la bibliothèque de base n'utilise la classe de l'extension que par réflexion, R8 peut supprimer la classe ou son constructeur d'un build de l'application, ce qui entraîne l'échec de la recherche. L'échec ne se produit que dans les builds des applications qui incluent l'extension. Il est donc difficile à détecter lors des tests.
Sans Jetpack Keep, l'extension fournit une règle de conservation du consommateur pour sa classe, comme indiqué dans Réflexion pour prendre en charge les dépendances facultatives :
# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Cette règle se trouve dans une bibliothèque différente de la recherche qui en a besoin. Les deux peuvent donc être désynchronisés. Avec Jetpack Keep, vous annotez plutôt la recherche dans la bibliothèque de base :
// 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'annotation suit ces consignes :
- Annotez le code qui effectue la réflexion. L'annotation se trouve sur
initialize, qui appelleClass.forName, et non sur la classe de l'extension. - Choisissez l'annotation en fonction de ce que fait le code. La recherche crée une instance de la classe. Le code utilise donc
@UsesReflectionToConstruct. - Utilisez
classNamepour une classe à laquelle vous ne pouvez pas faire référence. La classe de l'extension ne se trouve pas dans le classpath de compilation de la bibliothèque de base. L'annotation ne peut donc pas utiliserclassConstant. - Ne conservez que le constructeur que le code appelle. La recherche appelle le constructeur sans argument.
parameterTypesest donc un tableau vide.
Lorsque vous compilez l'AAR de la bibliothèque de base, le plug-in ajoute une règle qui conserve VideoEventTracker et son constructeur sans argument chaque fois que R8 conserve initialize. Les applications qui utilisent le SDK appliquent automatiquement cette règle, même si elles n'utilisent pas Jetpack Keep. L'extension n'a donc pas besoin de sa propre règle.
Par rapport à la règle de l'extension, cette approche présente les avantages suivants :
- La règle reste à côté de la recherche. La bibliothèque de base possède à la fois la recherche et la règle. Vous les mettez donc à jour ensemble au lieu de coordonner une modification dans deux bibliothèques.
- R8 peut toujours supprimer le code inutilisé. Si une application inclut l'extension, mais n'appelle jamais
initialize, la règle générée ne s'applique pas et R8 peut supprimerVideoEventTracker. La règle de l'extension la conserve dans chaque application qui inclut l'extension. - Lint détecte les recherches non annotées. Si la bibliothèque de base ajoute ultérieurement une autre recherche réflexive sans annotation, lint la signale. Vous pouvez ainsi la corriger avant de publier la bibliothèque.
Pour confirmer le résultat, vérifiez que la règle générée apparaît dans le fichier de l'AAR's
proguard.txt, comme décrit dans Bibliothèques Android. Testez ensuite un build d'une application qui inclut l'extension, car c'est là qu'une règle manquante interromprait l'analyse vidéo.
Ressources supplémentaires
- Exemple Jetpack Keep : projet Android Studio qui utilise Jetpack Keep dans une application, une bibliothèque Android, une bibliothèque multiplateforme Kotlin et une bibliothèque JVM.
- Cas d'utilisation et exemples de règles de conservation : règles de conservation pour les scénarios courants de réflexion et JNI.
- Suivez les bonnes pratiques : conseils pour rédiger des règles de conservation à portée limitée.