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.
Reflet
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 des 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 les classes qu'il considère comme inutilisées.
Prenons l'exemple suivant dans lequel vous disposez d'une bibliothèque et d'une application qui l'utilise. Le code illustre 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 contient 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 elle n'utilise pas directement la classe, ce qui interrompt l'intégration. La règle recherche 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.
Réflexion avec ::class.java
Les bibliothèques peuvent charger des classes en faisant passer directement l'objet Class par l'application, ce qui est une méthode plus robuste que le chargement de classes par nom. Cela crée une référence forte à la classe que R8 peut détecter. Toutefois, bien que cela empêche R8 de supprimer la classe, vous devez toujours utiliser une règle de conservation pour déclarer que la classe est instanciée de manière réflexive et pour protéger les membres auxquels vous accédez 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 l'utilise. Le chargeur de bibliothèque instancie une StartupTask
interface 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 contient 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 garantissant le bon fonctionnement du code. 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 votreStartupTaskinterface.-keep class * implements com.example.library.StartupTask: cette règle conserve toute classe (*) qui implémente votre interface.,allowobfuscation: cette règle indique à R8 que, malgré la conservation de la classe, il peut la renommer ou l'obscurcir. Cela ne pose aucun problème, 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 deStartupTaskqui 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: cette règle conserve des membres spécifiques (méthodes, champs) de la classe qui implémenteStartupTaskinterface, mais uniquement si la classe implémentée elle-même est conservée.{ <init>(); }: il s'agit du sélecteur de membre.<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 voit 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 uneInstantiationException.
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 baliser 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.
Prenons l'exemple suivant dans lequel vous disposez d'une bibliothèque et d'une application qui l'utilise. L'exemple illustre un bus d'événements qui trouve et appelle des 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 contient 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 conserve 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 conserve les annotations destinées à être lues au moment de l'exécution.-keep @interface com.example.library.OnEvent: cette règle conserve la classe d'annotationOnEventelle-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>;: cette règle conserve 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 d'exécution de tâches trouve toutes les classes annotées avec ReflectiveExecutor à l'aide de la réflexion et exécute la méthode execute.
Prenons l'exemple suivant 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 contient 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 de manière réflexive 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 logicielle entre une bibliothèque principale et une bibliothèque complémentaire 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 distribuer 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 la classe est trouvée, la fonctionnalité est activée. Sinon, elle échoue de manière contrôlée.
Prenons l'exemple du code suivant, dans lequel un AnalyticsManager principal 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 la trouver.
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Sans cette règle, R8 supprime probablement VideoEventTracker de la bibliothèque facultative, car rien dans ce module ne l'utilise directement. La règle de conservation conserve la classe et son constructeur, ce qui permet à la bibliothèque principale de l'instancier.
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 est susceptible d'être modifié sans préavis, ce qui peut entraîner un comportement inattendu ou des plantages dans votre application.
Lorsque vous utilisez la réflexion pour des 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 plus récentes de la bibliothèque.
- Avantages manqués : vous risquez de passer à côté de nouvelles fonctionnalités, de corrections de plantages importantes ou de mises à jour de sécurité essentielles.
Optimisations R8 et réflexion
Si vous devez refléter le code privé ou protégé d'une bibliothèque, faites très attention aux optimisations de R8. S'il n'existe aucune référence directe à ces membres, R8 peut supposer qu'ils ne sont pas utilisés et les supprimer ou les renommer.
Cela peut entraîner des plantages au moment de l'exécution, souvent avec des messages d'erreur trompeurs tels que NoSuchMethodException ou NoSuchFieldException.
Prenons l'exemple suivant qui montre comment accéder à un champ privé à partir d'une classe de bibliothèque.
Une bibliothèque que vous ne possédez pas contient le code suivant :
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
Votre application contient 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: cette règle conserve des membres spécifiques d'une classe uniquement si la classe elle-même est conservée.class com.example.LibraryClass: cette règle cible la classe exacte contenant le champ.private java.lang.String secretMessage;: cette règle 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 ascendants du code natif (C/C++) vers Java ou Kotlin. Bien que l'inverse soit également vrai (les appels descendants de Java ou Kotlin vers le code natif peuvent poser problème), 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 ascendants du code natif (C/C++) vers Java ou Kotlin, R8 ne peut pas voir les méthodes appelées à partir de votre code natif. S'il n'existe aucune référence directe à ces méthodes dans votre application, R8 suppose à tort qu'elles ne sont pas utilisées et les supprime, ce qui entraîne 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 des 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 d'abord instanciée dans le code Kotlin ou Java. Elle indique à R8 que l'application utilise la classe et qu'elle doit conserver des membres spécifiques de la classe.-keepclassmembers: cette règle conserve des membres spécifiques d'une classe uniquement si la classe est d'abord instanciée dans le code Kotlin ou Java. Elle indique à R8 que l'application utilise la classe et qu'elle doit conserver des membres spécifiques de la classe.class com.example.JniBridge: cette règle cible la classe exacte contenant le champ.includedescriptorclasses: ce modificateur conserve é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);: cette règle spécifie la signature Java exacte de la méthode à conserver.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: Bien qu'includedescriptorclassesgarantisse que la classeNativeDataelle-même est conservée, tous les membres (champs ou méthodes) deNativeDataauxquels vous accédez directement à partir de votre code JNI natif ont besoin de leurs propres règles de conservation.-keep class NativeData: cette règle cible la classe nomméeNativeDataet le bloc spécifie les membres à conserver dans la classeNativeData.<init>(java.lang.Integer, java.lang.String): il s'agit de la signature du constructeur. Elle identifie de manière unique le constructeur qui accepte deux paramètres : le premier est unIntegeret le second est uneString.
Appels de plate-forme indirects
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.
Prenons l'exemple suivant dans lequel 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, il n'existe aucune règle de conservation 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 plante lorsque le système tente 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, tenez compte des points suivants :
- Le plug-in crée automatiquement des champs
CREATORlors de la compilation. - Le fichier
proguard-android-optimize.txtcontient les règleskeepnécessaires pour conserver ces champs et assurer le bon fonctionnement. - Les développeurs d'applications doivent vérifier que toutes les règles
keeprequises sont présentes, en particulier pour les implémentations personnalisées ou les dépendances tierces.
Bibliothèques populaires
Les bibliothèques qui utilisent la réflexion ou les transformations de bytecode accèdent au code de manière dynamique au moment de l'exécution. Si R8 supprime ou renomme des classes, des champs ou des méthodes auxquels vous accédez de cette manière, votre application peut planter.
Toutefois, les bibliothèques tierces populaires (telles que Gson, Retrofit et Kotlinx Serialization) regroupent automatiquement leurs propres règles de conservation du consommateur R8. Lorsque vous utilisez des versions récentes de ces bibliothèques, vous n'avez pas besoin d'ajouter manuellement des règles de conservation à votre projet.
gson
Gson est une bibliothèque de sérialisation et de désérialisation JSON qui repose fortement sur la réflexion. Lorsque vous utilisez le mode complet pour optimiser votre application, elle supprime les signatures de type générique, les constructeurs par défaut et les champs non annotés, sauf indication contraire.
Pour vous assurer que Gson fonctionne correctement, ajoutez des règles spécifiques pour conserver les champs non transitoires dans vos classes de modèle de données et préserver la hiérarchie TypeToken :
# Preserve generic type information required for deserialization
-keepattributes Signature
# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
!transient <fields>;
}
# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
Les champs marqués du modificateur transient sont ignorés par Gson lors de la
sérialisation et de la désérialisation. C'est pourquoi la règle de conservation cible spécifiquement les
champs non transitoires (!transient).
Modernisation
Retrofit est une bibliothèque réseau qui inspecte les méthodes d'interface de service annotées avec des annotations HTTP (telles que @GET ou @POST) à l'aide de la réflexion pour construire des requêtes réseau et convertir des réponses.
Retrofit génère dynamiquement des implémentations de vos interfaces API au moment de l'exécution à l'aide de Proxy.newProxyInstance(). Étant donné que R8 ne voit aucune classe implémenter ces interfaces de manière statique, il peut supprimer les méthodes ou leurs types de retour génériques.
Règles de conservation regroupées
Retrofit s'appuie sur la réflexion au moment de l'exécution pour inspecter les paramètres génériques, les annotations de méthode et les annotations de paramètre. Sans configuration appropriée, le mode complet R8 peut supprimer complètement les signatures génériques des types de retour, des continuations Kotlin et des classes de réponse, ou même remplacer les valeurs d'interface par null, car les interfaces Retrofit sont instanciées dynamiquement avec un proxy.
À partir de Retrofit 2.10.0, la bibliothèque regroupe automatiquement les règles de conservation officielles requises pour conserver les valeurs par défaut des annotations, les paramètres de méthode de service et les métadonnées de classe nécessaires. Pour en savoir plus, consultez Règles utilisées par Retrofit.
Conserver les types de retour génériques
Retrofit inspecte la signature générique du type renvoyé (par exemple, Observable<Data>) pour désérialiser correctement la réponse réseau. Si R8 supprime la signature générique, Retrofit remplace l'objet instancié par null.
Pour empêcher le mode complet R8 de supprimer la signature générique de vos types de retour, utilisez la règle conditionnelle suivante :
# Preserve generic type information for Call/Observable return types
-keepattributes Signature
# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
@retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
La classe de modèle de données réelle renvoyée (par exemple, Data dans
Observable<Data>) doit également être conservée, car elle sera construite de manière réflexive par
le convertisseur (comme Gson).
Coroutines
Lorsque vous utilisez des coroutines Kotlin, le compilateur Kotlin transforme les fonctions suspend en ajoutant un paramètre Continuation à la signature de la méthode compilée.
Lorsque des bibliothèques telles que Retrofit lisent de manière réflexive la signature générique d'une fonction suspend, elles s'appuient sur ce paramètre Continuation. En mode complet, l'attribut Signature n'est conservé que pour les classes explicitement conservées. Étant donné que Continuation est un paramètre synthétique, R8 supprime sa signature par défaut, ce qui interrompt la réflexion.
Pour éviter la suppression de la signature et garantir la compatibilité au moment de l'exécution en mode complet, incluez la règle suivante :
# Keep the signature attribute globally
-keepattributes Signature
# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation