R8 propose deux modes : le mode de compatibilité et le mode complet. Le mode complet vous offre des optimisations puissantes qui améliorent les performances de votre application.
Ce guide s'adresse aux développeurs Android qui souhaitent utiliser les optimisations les plus puissantes de R8. Il explore les principales différences entre le mode compatibilité et le mode complet, et fournit les configurations explicites nécessaires pour migrer votre projet en toute sécurité et éviter les plantages d'exécution courants.
Activer le mode complet
Pour activer le mode complet, supprimez la ligne suivante de votre fichier gradle.properties :
android.enableR8.fullMode=false // Remove this line to enable full mode
Conserver les classes associées aux attributs
Les attributs sont des métadonnées stockées dans des fichiers de classe compilés qui ne font pas partie du code exécutable. Toutefois, ils peuvent être nécessaires pour certains types de réflexion. Parmi les exemples courants, citons Signature (qui préserve les informations de type générique après l'effacement de type), InnerClasses et EnclosingMethod (pour la réflexion sur la structure de classe) et les annotations visibles au moment de l'exécution.
Le code suivant montre à quoi ressemble un attribut Signature pour un champ dans le bytecode. Pour un champ :
List<User> users;
Le fichier de classe compilé contiendrait le bytecode suivant :
.field public static final users:Ljava/util/List;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/util/List<",
"Lcom/example/package/User;",
">;"
}
.end annotation
.end field
Les bibliothèques qui utilisent beaucoup la réflexion (comme Gson) s'appuient souvent sur ces attributs pour inspecter et comprendre dynamiquement la structure de votre code. Par défaut, en mode complet de R8, les attributs ne sont conservés que si la classe, le champ ou la méthode associés sont explicitement conservés.
L'exemple suivant montre pourquoi les attributs sont nécessaires et quelles règles de conservation vous devez ajouter lorsque vous passez du mode compatibilité au mode complet.
Prenons l'exemple suivant, dans lequel nous désérialisons une liste d'utilisateurs à l'aide de la bibliothèque Gson.
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
data class User(
@SerializedName("username")
var username: String? = null,
@SerializedName("age")
var age: Int = 0
)
fun GsonRemoteJsonListExample() {
val gson = Gson()
// 1. The JSON string for a list of users returned from remote
val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""
// 2. Deserialize the JSON string into a List<User>
// We must use TypeToken for generic types like List
val listType = object : TypeToken<List<User>>() {}.type
val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)
// Print the list
println("First user from list: ${deserializedList}")
}
Lors de la compilation, l'effacement de type de Java supprime les arguments de type génériques. Cela signifie qu'au moment de l'exécution, List<String> et List<User> apparaissent sous la forme d'un List brut. Par conséquent, les bibliothèques telles que Gson, qui s'appuient sur la réflexion, ne peuvent pas déterminer les types d'objets spécifiques que List a été déclaré contenir lors de la désérialisation d'une liste JSON, ce qui peut entraîner des problèmes d'exécution.
Pour préserver les informations sur les types, Gson utilise TypeToken. L'encapsulation TypeToken conserve les informations de désérialisation nécessaires.
L'expression Kotlin object:TypeToken<List<User>>() {}.type crée une classe interne anonyme qui étend TypeToken et capture les informations de type générique. Dans cet exemple, la classe anonyme est nommée $GsonRemoteJsonListExample$listType$1.
Le langage de programmation Java enregistre la signature générique d'une superclasse en tant que métadonnées, appelées attribut Signature, dans le fichier de classe compilé.
TypeToken utilise ensuite ces métadonnées Signature pour récupérer le type au moment de l'exécution.
Cela permet à Gson d'utiliser la réflexion pour lire Signature et découvrir le type List<User> complet dont il a besoin pour la désérialisation.
Lorsque R8 est activé en mode compatibilité, il conserve l'attribut Signature pour les classes, y compris les classes internes anonymes telles que $GsonRemoteJsonListExample$listType$1, même si des règles de conservation spécifiques ne sont pas explicitement définies. Par conséquent, le mode compatibilité R8 ne nécessite aucune règle de conservation explicite supplémentaire pour que cet exemple fonctionne comme prévu.
// keep rule for compatibility mode
-keepattributes Signature
Lorsque R8 est activé en mode complet, l'attribut Signature de la classe interne anonyme $GsonRemoteJsonListExample$listType$1 est supprimé. Sans ces informations de type dans Signature, Gson ne peut pas trouver le type d'application approprié, ce qui entraîne une erreur IllegalStateException. Voici les règles de conservation nécessaires pour éviter cela :
// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
-keepattributes Signature: cette règle indique à R8 de conserver l'attribut dont Gson a besoin pour la lecture. En mode complet, R8 ne conserve l'attributSignatureque pour les classes, les champs ou les méthodes qui correspondent explicitement à une règlekeep.-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: cette règle est nécessaire, carTypeTokenencapsule le type de l'objet en cours de désérialisation. Après l'effacement de type, une classe interne anonyme est créée pour conserver les informations sur le type générique. Sans conserver explicitementcom.google.gson.reflect.TypeToken, R8 en mode complet n'inclura pas ce type de classe dans l'attributSignaturenécessaire à la désérialisation.-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: cette règle conserve les informations de type des classes anonymes qui étendentTypeToken, comme$GsonRemoteJsonListExample$listType$1dans cet exemple. Sans cette règle, R8 en mode complet supprime les informations de type nécessaires, ce qui entraîne l'échec de la désérialisation.
À partir de la version 2.11.0 de Gson, la bibliothèque regroupe les règles de conservation nécessaires pour la désérialisation en mode complet. Lorsque vous compilez votre application avec R8 activé, R8 trouve et applique automatiquement ces règles à partir de la bibliothèque. Cela fournit la protection dont votre application a besoin sans que vous ayez à ajouter ni à gérer manuellement ces règles spécifiques dans votre projet.
Il est important de comprendre que les règles partagées précédemment ne résolvent que le problème de la découverte du type générique (par exemple, List<User>). R8 renomme également les champs des classes. Si vous n'utilisez pas d'annotations @SerializedName sur vos modèles de données, Gson ne pourra pas désérialiser le JSON, car les noms de champs ne correspondront plus aux clés JSON.
Toutefois, si vous utilisez une version de Gson antérieure à 2.11 ou si vos modèles n'utilisent pas l'annotation @SerializedName, vous devez ajouter des règles de conservation explicites pour ces modèles.
Conserver le constructeur par défaut
En mode complet R8, le constructeur par défaut/sans arguments n'est pas conservé de manière implicite, même lorsque la classe elle-même est conservée. Si vous créez une instance d'une classe à l'aide de class.getDeclaredConstructor().newInstance() ou class.newInstance(), vous devez conserver explicitement le constructeur sans arguments en mode complet. En revanche, le mode de compatibilité conserve toujours le constructeur sans arguments.
Prenons l'exemple d'une instance de PrecacheTask créée à l'aide de la réflexion pour appeler dynamiquement sa méthode run. Bien que ce scénario ne nécessite pas de règles supplémentaires en mode Compatibilité, le constructeur par défaut de PrecacheTask serait supprimé en mode Complet. Par conséquent, une règle de conservation spécifique est requise.
// In library
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()
}
}
// In app
class PreCacheTask : StartupTask {
override fun run() {
Log.d("Pre cache task", "Warming up the cache...")
}
}
fun runTaskRunner() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified
-keep class com.example.fullmoder8.PreCacheTask {
<init>();
}
La modification de l'accès est activée par défaut
En mode de compatibilité, R8 ne modifie pas la visibilité des méthodes et des champs dans une classe. Toutefois, en mode complet, R8 améliore l'optimisation en modifiant la visibilité de vos méthodes et champs (par exemple, en la faisant passer de privée à publique). Cela permet d'intégrer davantage de contenu.
Cette optimisation peut poser problème si votre code utilise une réflexion qui repose spécifiquement sur la visibilité de certains membres. R8 ne reconnaîtra pas cette utilisation indirecte, ce qui peut entraîner des plantages de l'application. Pour éviter cela, vous devez ajouter des règles -keep spécifiques pour conserver les membres, ce qui préservera également leur visibilité d'origine.
Pour en savoir plus, consultez cet exemple. Vous comprendrez ainsi pourquoi il est déconseillé d'accéder aux membres privés à l'aide de la réflexion, et vous découvrirez les règles de conservation permettant de conserver ces champs/méthodes.