Les exemples suivants sont basés sur des scénarios courants dans lesquels vous utilisez R8 pour l'optimisation, mais avez besoin de conseils avancés pour rédiger des règles de conservation.
Réflexion
En général, pour des performances optimales, il n'est pas recommandé d'utiliser la réflexion. Toutefois, dans certains cas, cela peut être inévitable. Les exemples suivants fournissent des conseils pour les règles de conservation dans les scénarios courants qui utilisent la réflexion.
Réflexion avec des classes chargées par nom
Les bibliothèques chargent souvent les classes de manière dynamique en utilisant le nom de la classe comme String
.
Toutefois, R8 ne peut pas détecter les classes chargées de cette manière et peut supprimer celles qu'il considère comme inutilisées.
Prenons l'exemple suivant où vous disposez d'une bibliothèque et d'une application qui l'utilise. Le code montre un chargeur de bibliothèque qui instancie une interface StartupTask
implémentée par une application.
Le code de la bibliothèque est le suivant :
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(className: String) {
// R8 won't retain classes specified by this string value at runtime
val taskClass = Class.forName(className)
val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
task.run()
}
}
L'application qui utilise la bibliothèque comporte le code suivant :
// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
override fun run() {
// This log will never appear if the class is removed by R8.
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is told to run the app's task by its name.
TaskRunner.execute("com.example.app.PreCacheTask")
}
Dans ce scénario, votre bibliothèque doit inclure un fichier de règles de conservation du consommateur avec les règles de conservation suivantes :
-keep class * implements com.example.library.StartupTask {
<init>();
}
Sans cette règle, R8 supprime PreCacheTask
de l'application, car celle-ci n'utilise pas la classe directement, ce qui interrompt l'intégration. La règle trouve les classes qui implémentent l'interface StartupTask
de votre bibliothèque et les conserve, ainsi que leur constructeur sans argument, ce qui permet à la bibliothèque d'instancier et d'exécuter PreCacheTask
avec succès.
Réflexion avec ::class.java
Les bibliothèques peuvent charger des classes en demandant à l'application de transmettre directement l'objet Class
, ce qui est une méthode plus robuste que le chargement des classes par nom. Cela crée une référence forte à la classe que R8 peut détecter. Toutefois, même si cela empêche R8 de supprimer la classe, vous devez toujours utiliser une règle keep pour déclarer que la classe est instanciée de manière réflexive et pour protéger les membres auxquels on accède de manière réflexive, comme le constructeur.
Prenons l'exemple suivant dans lequel vous disposez d'une bibliothèque et d'une application qui utilise la bibliothèque. Le chargeur de bibliothèque instancie une interface StartupTask
en transmettant directement la référence de classe.
Le code de la bibliothèque est le suivant :
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(taskClass: Class<out StartupTask>) {
// The class isn't removed, but its constructor might be.
val task = taskClass.getDeclaredConstructor().newInstance()
task.run()
}
}
L'application qui utilise la bibliothèque comporte le code suivant :
// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
override fun run() {
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
Dans ce scénario, votre bibliothèque doit inclure un fichier de règles de conservation du consommateur avec les règles de conservation suivantes :
# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
<init>();
}
Ces règles sont conçues pour fonctionner parfaitement avec ce type de réflexion, ce qui permet une optimisation maximale tout en s'assurant que le code fonctionne correctement. Les règles permettent à R8 d'obscurcir le nom de la classe et de réduire ou de supprimer l'implémentation de la classe StartupTask
si l'application ne l'utilise jamais. Toutefois, pour toute implémentation, telle que PrecacheTask
utilisée dans l'exemple, elles conservent le constructeur par défaut (<init>()
) que votre bibliothèque doit appeler.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
: cette règle cible toute classe qui implémente votre interfaceStartupTask
.-keep class * implements com.example.library.StartupTask
: cela préserve toute classe (*
) qui implémente votre interface.,allowobfuscation
: indique à R8 que, même si la classe est conservée, elle peut être renommée ou obscurcie. Cette opération est sûre, car votre bibliothèque ne repose pas sur le nom de la classe. Elle obtient directement l'objetClass
.,allowshrinking
: ce modificateur indique à R8 qu'il peut supprimer la classe si elle n'est pas utilisée. Cela permet à R8 de supprimer en toute sécurité une implémentation deStartupTask
qui n'est jamais transmise àTaskRunner.execute()
. En bref, cette règle implique ce qui suit : si une application utilise une classe qui implémenteStartupTask
, R8 conserve la classe. R8 peut renommer la classe pour réduire sa taille et la supprimer si l'application ne l'utilise pas.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }
: cette règle cible des membres spécifiques des classes identifiées dans la première règle (dans ce cas, le constructeur).-keepclassmembers class * implements com.example.library.StartupTask
: cela permet de conserver des membres spécifiques (méthodes, champs) de la classe qui implémente l'interfaceStartupTask
, mais uniquement si la classe implémentée elle-même est conservée.{ <init>(); }
: il s'agit du sélecteur de membres.<init>
est le nom interne spécial d'un constructeur dans le bytecode Java. Cette partie cible spécifiquement le constructeur par défaut sans argument.- Cette règle est essentielle, car votre code appelle
getDeclaredConstructor().newInstance()
sans aucun argument, ce qui appelle de manière réflexive le constructeur par défaut. Sans cette règle, R8 constate qu'aucun code n'appelle directementnew PreCacheTask()
, suppose que le constructeur n'est pas utilisé et le supprime. Cela entraîne le plantage de votre application au moment de l'exécution avec unInstantiationException
.
Réflexion basée sur l'annotation de la méthode
Les bibliothèques définissent souvent des annotations que les développeurs utilisent pour taguer des méthodes ou des champs.
La bibliothèque utilise ensuite la réflexion pour trouver ces membres annotés au moment de l'exécution. Par exemple, l'annotation @OnLifecycleEvent
est utilisée pour trouver les méthodes requises au moment de l'exécution.
Par exemple, prenons le scénario suivant dans lequel vous disposez d'une bibliothèque et d'une application qui utilise la bibliothèque. L'exemple montre un bus d'événements qui trouve et appelle les méthodes annotées avec @OnEvent
.
Le code de la bibliothèque est le suivant :
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent
class EventBus {
fun dispatch(listener: Any) {
// Find all methods annotated with @OnEvent and invoke them
listener::class.java.declaredMethods.forEach { method ->
if (method.isAnnotationPresent(OnEvent::class.java)) {
try {
method.invoke(listener)
} catch (e: Exception) { /* ... */ }
}
}
}
}
L'application qui utilise la bibliothèque comporte le code suivant :
class MyEventListener {
@OnEvent
fun onSomethingHappened() {
// This method will be removed by R8 without a keep rule
Log.d(TAG, "Event received!")
}
}
fun onCreate() {
// Instantiate the listener and the event bus
val listener = MyEventListener()
val eventBus = EventBus()
// Dispatch the listener to the event bus
eventBus.dispatch(listener)
}
La bibliothèque doit inclure un fichier de règles de conservation du consommateur qui préserve automatiquement toutes les méthodes utilisant ses annotations :
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations
: cette règle préserve les annotations destinées à être lues lors de l'exécution.-keep @interface com.example.library.OnEvent
: cette règle préserve la classe d'annotationOnEvent
elle-même.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}
: cette règle conserve une classe et des membres spécifiques uniquement si la classe est utilisée et qu'elle contient ces membres.-keepclassmembers
: cette règle conserve une classe et des membres spécifiques uniquement si la classe est utilisée et qu'elle contient ces membres.class *
: la règle s'applique à n'importe quelle classe.@com.example.library.OnEvent <methods>;
: cela permet de conserver toute classe comportant une ou plusieurs méthodes (<methods>
) annotées avec@com.example.library.OnEvent
, ainsi que les méthodes annotées elles-mêmes.
Réflexion basée sur les annotations de classe
Les bibliothèques peuvent utiliser la réflexion pour rechercher des classes comportant une annotation spécifique. Dans ce cas, la classe du task runner trouve toutes les classes annotées avec ReflectiveExecutor
à l'aide de la réflexion et exécute la méthode execute
.
Prenons l'exemple d'un scénario dans lequel vous disposez d'une bibliothèque et d'une application qui l'utilise.
La bibliothèque contient le code suivant :
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor
class TaskRunner {
fun process(task: Any) {
val taskClass = task::class.java
if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
val methodToCall = taskClass.getMethod("execute")
methodToCall.invoke(task)
}
}
}
L'application qui utilise la bibliothèque comporte le code suivant :
// In consumer app
@ReflectiveExecutor
class ImportantBackgroundTask {
fun execute() {
// This class will be removed by R8 without a keep rule
Log.e("ImportantBackgroundTask", "Executing the important background task...")
}
}
// Usage of ImportantBackgroundTask
fun onCreate(){
val task = ImportantBackgroundTask()
val runner = TaskRunner()
runner.process(task)
}
Étant donné que la bibliothèque utilise la réflexion pour obtenir des classes spécifiques, elle doit inclure un fichier de règles de conservation du consommateur avec les règles de conservation suivantes :
# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations
# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor
# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
public void execute();
}
Cette configuration est très efficace, car elle indique à R8 exactement ce qu'il doit conserver.
Réflexion pour prendre en charge les dépendances facultatives
Un cas d'utilisation courant de la réflexion consiste à créer une dépendance faible entre une bibliothèque principale et une bibliothèque de modules complémentaires facultative. La bibliothèque principale peut vérifier si le module complémentaire est inclus dans l'application et, le cas échéant, activer des fonctionnalités supplémentaires. Cela vous permet de fournir des modules complémentaires sans forcer la bibliothèque principale à avoir une dépendance directe sur eux.
La bibliothèque principale utilise la réflexion (Class.forName
) pour rechercher une classe spécifique par son nom. Si le cours est trouvé, la fonctionnalité est activée. Sinon, l'opération échoue de manière contrôlée.
Par exemple, prenons le code suivant, dans lequel un AnalyticsManager
de base recherche une classe VideoEventTracker
facultative pour activer l'analyse vidéo.
La bibliothèque principale contient le code suivant :
object AnalyticsManager {
private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
fun initialize() {
try {
// Attempt to load the optional module's class using reflection
Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
Log.d(TAG, "Video tracking enabled.")
} catch (e: ClassNotFoundException) {
Log.d(TAG,"Video tracking module not found. Skipping.")
} catch (e: Exception) {
Log.e(TAG, e.printStackTrace())
}
}
}
La bibliothèque vidéo facultative contient le code suivant :
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
Le développeur de la bibliothèque facultative est responsable de la fourniture de la règle de conservation du consommateur nécessaire. Cette règle de conservation garantit que toute application utilisant la bibliothèque facultative conserve le code dont la bibliothèque principale a besoin pour trouver.
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Sans cette règle, R8 supprimera probablement VideoEventTracker
de la bibliothèque facultative, car rien dans ce module ne l'utilise directement. La règle de conservation préserve la classe et son constructeur, ce qui permet à la bibliothèque principale de l'instancier correctement.
Réflexion pour accéder aux membres privés
L'utilisation de la réflexion pour accéder à du code privé ou protégé qui ne fait pas partie de l'API publique d'une bibliothèque peut entraîner des problèmes importants. Ce code peut être modifié sans préavis, ce qui peut entraîner des comportements inattendus ou des plantages dans votre application.
Lorsque vous vous appuyez sur la réflexion pour les API non publiques, vous pouvez rencontrer les problèmes suivants :
- Mises à jour bloquées : les modifications apportées au code privé ou protégé peuvent vous empêcher de passer à des versions ultérieures de la bibliothèque.
- Avantages manqués : vous risquez de passer à côté de nouvelles fonctionnalités, de correctifs importants en cas de plantage ou de mises à jour de sécurité essentielles.
Optimisations R8 et réflexion
Si vous devez refléter dans le code privé ou protégé d'une bibliothèque, portez une attention particulière aux optimisations de R8. S'il n'y a pas de références directes à ces membres, R8 peut supposer qu'ils ne sont pas utilisés et les supprimer ou les renommer par la suite.
Cela peut entraîner des plantages d'exécution, souvent avec des messages d'erreur trompeurs tels que NoSuchMethodException
ou NoSuchFieldException
.
Par exemple, prenons le scénario suivant qui montre comment accéder à un champ privé à partir d'une classe de bibliothèque.
Une bibliothèque qui ne vous appartient pas comporte le code suivant :
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
Votre application comporte le code suivant :
fun accessSecretMessage(instance: LibraryClass) {
// Use Java reflection from Kotlin to access the private field
val secretField = instance::class.java.getDeclaredField("secretMessage")
secretField.isAccessible = true
// This will crash at runtime with R8 enabled
val message = secretField.get(instance) as String
}
Ajoutez une règle -keep
dans votre application pour empêcher R8 de supprimer le champ privé :
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers
: cela permet de conserver des membres spécifiques d'une classe uniquement si la classe elle-même est conservée.class com.example.LibraryClass
: cible la classe exacte contenant le champ.private java.lang.String secretMessage;
: identifie le champ privé spécifique par son nom et son type.
Java Native Interface (JNI)
Les optimisations de R8 peuvent poser problème lorsque vous utilisez des appels de procédure inverse du code natif (C/C++) vers Java ou Kotlin. L'inverse est également vrai : les appels descendants de Java ou Kotlin vers le code natif peuvent poser problème. Toutefois, le fichier par défaut proguard-android-optimize.txt
inclut la règle suivante pour que les appels descendants fonctionnent. Cette règle empêche la suppression des méthodes natives.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Interaction avec le code natif via Java Native Interface (JNI)
Lorsque votre application utilise JNI pour effectuer des appels depuis du code natif (C/C++) vers Java ou Kotlin, R8 ne peut pas voir quelles méthodes sont appelées depuis votre code natif. S'il n'y a aucune référence directe à ces méthodes dans votre application, R8 suppose à tort que ces méthodes ne sont pas utilisées et les supprime, ce qui provoque le plantage de votre application.
L'exemple suivant montre une classe Kotlin avec une méthode destinée à être appelée à partir d'une bibliothèque native. La bibliothèque native instancie un type d'application et transmet les données du code natif au code Kotlin.
package com.example.models
// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
/**
* This method is called from the native side.
* R8 will remove it if it's not kept.
*/
fun onNativeEvent(data: NativeData) {
Log.d(TAG, "Received event from native code: $data")
}
// Use 'external' to declare a native method
external fun startNativeProcess()
companion object {
init {
// Load the native library
System.loadLibrary("my-native-lib")
}
}
}
Dans ce cas, vous devez informer R8 pour empêcher l'optimisation du type d'application. De plus, si les méthodes appelées à partir du code natif utilisent vos propres classes dans leurs signatures en tant que paramètres ou types de retour, vous devez également vérifier que ces classes ne sont pas renommées.
Ajoutez les règles de conservation suivantes à votre application :
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
Ces règles de conservation empêchent R8 de supprimer ou de renommer la méthode onNativeEvent
et, surtout, son type de paramètre.
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}
: cette règle conserve des membres spécifiques d'une classe uniquement si la classe est instanciée en code Kotlin ou Java en premier. Elle indique à R8 que l'application utilise la classe et qu'il doit conserver des membres spécifiques de la classe.-keepclassmembers
: cela permet de conserver des membres spécifiques d'une classe uniquement si la classe est instanciée en code Kotlin ou Java en premier. Cela indique à R8 que l'application utilise la classe et qu'il doit conserver des membres spécifiques de la classe.class com.example.JniBridge
: cible la classe exacte contenant le champ.includedescriptorclasses
: ce modificateur préserve également toutes les classes trouvées dans la signature ou le descripteur de la méthode. Dans ce cas, il empêche R8 de renommer ou de supprimer la classecom.example.models.NativeData
, qui est utilisée comme paramètre. SiNativeData
était renommé (par exemple, ena.a
), la signature de la méthode ne correspondrait plus à ce que le code natif attend, ce qui entraînerait un plantage.public void onNativeEvent(com.example.models.NativeData);
: spécifie la signature Java exacte de la méthode à conserver.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}
: siincludedescriptorclasses
garantit que la classeNativeData
elle-même est conservée, tous les membres (champs ou méthodes) deNativeData
auxquels vous accédez directement à partir de votre code JNI natif ont besoin de leurs propres règles de conservation.-keep class NativeData
: cible la classe nomméeNativeData
, et le bloc spécifie les membres à conserver dans la classeNativeData
.<init>(java.lang.Integer, java.lang.String)
: signature du constructeur. Il identifie de manière unique le constructeur qui accepte deux paramètres : le premier est unInteger
et le second est unString
.
Appels indirects de la plate-forme
Transférer des données avec une implémentation de Parcelable
Le framework Android utilise la réflexion pour créer des instances de vos objets Parcelable
. Dans le développement Kotlin moderne, vous devez utiliser le plug-in kotlin-parcelize
, qui génère automatiquement l'implémentation Parcelable
nécessaire, y compris le champ CREATOR
et les méthodes dont le framework a besoin.
Par exemple, prenons l'exemple suivant où le plug-in kotlin-parcelize
est utilisé pour créer une classe Parcelable
:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
val name: String,
val age: Int
) : Parcelable
Dans ce scénario, aucune règle de conservation n'est recommandée. Le plug-in Gradle kotlin-parcelize
génère automatiquement les règles de conservation requises pour les classes que vous annotez avec @Parcelize
. Il gère la complexité pour vous, en veillant à ce que les CREATOR
et les constructeurs générés soient conservés pour les appels de réflexion du framework Android.
Si vous écrivez manuellement une classe Parcelable
en Kotlin sans utiliser @Parcelize
, vous êtes responsable de la conservation du champ CREATOR
et du constructeur qui accepte un Parcel
. Si vous oubliez de le faire, votre application plantera lorsque le système tentera de désérialiser votre objet. L'utilisation de @Parcelize
est la pratique standard et la plus sûre.
Lorsque vous utilisez le plug-in kotlin-parcelize
, gardez à l'esprit ce qui suit :
- Le plug-in crée automatiquement des champs
CREATOR
lors de la compilation. - Le fichier
proguard-android-optimize.txt
contient les règleskeep
nécessaires pour conserver ces champs et assurer le bon fonctionnement. - Les développeurs d'applications doivent vérifier que toutes les règles
keep
requises sont présentes, en particulier pour les implémentations personnalisées ou les dépendances tierces.