Les outils de build d'Android viennent de recevoir un boost de performance pour les Kotlin Coroutines. Avec l'Android Gradle Plugin (AGP) 9.2.0 et le shrinker R8 inclus, le bytecode qui gère l'état des coroutines est réécrit pour offrir environ deux fois plus de performances lors des opérations de coroutine les plus courantes. Les développeurs qui publient des applications Android reposant sur les coroutines peuvent constater des temps de démarrage à froid (cold-start) plus rapides, des taux de rafraîchissement plus stables et une légère amélioration de l'autonomie de la batterie, sans changer une seule ligne de code Kotlin.
Pourquoi la performance des coroutines est importante
Les Kotlin Coroutines utilisent des objets AtomicFieldUpdater pour modifier les champs internes en toute sécurité depuis plusieurs threads. Cette approche évite d'allouer un nouvel objet atomique pour chaque mise à jour, mais elle introduit trois coûts cachés sur Android :
- Délai de chargement des classes – l'updater est créé par réflexion, le VM doit donc rechercher le champ cible au moment de l'exécution.
- Gaspillage de CPU – chaque accès vérifie l'état de l'updater avant d'atteindre le champ réel.
- Limites d'inlining – le compilateur ne peut pas inliner les appels à l'updater, ce qui maintient un bytecode généré plus volumineux et plus lent.
En pratique, ces coûts se manifestent lors des chemins critiques (hot paths) tels que l'envoi/réception de canaux (channel send/receive), le verrouillage/déverrouillage de mutex (mutex lock/unlock), les mises à jour de StateFlow et le dispatch des coroutines. Le résultat est une quantité notable de temps CPU consacrée à la gestion administrative plutôt qu'au travail propre de l'application.
Ce qui a changé dans AGP 9.2.0 et R8
R8, l'outil de réduction de code (code-shrinker) fourni avec AGP 9.2.0, scanne désormais le bytecode compilé pour détecter le motif standard AtomicFieldUpdater. Lorsqu'il en trouve un, il effectue quatre transformations :
- Identifier l'offset mémoire du champ. R8 calcule l'emplacement exact du champ cible à l'intérieur de la structure de l'objet.
- Supprimer l'objet updater. L'enveloppe par réflexion disparaît, économisant de la mémoire et éliminant le travail de chargement des classes.
- Insérer un appel direct à
sun.misc.Unsafe. Cette API de bas niveau écrit dans le champ en utilisant une seule instruction matérielle atomique. - Remplacer chaque appel à l'updater par la nouvelle instruction unsafe, permettant au compilateur JIT d'inliner l'opération.
L'effet net est que le CPU n'a plus besoin d'effectuer de recherche par réflexion ou de vérifications au moment de l'exécution ; il exécute directement l'instruction atomique. Du point de vue du développeur, le changement est invisible – l'API des coroutines se comporte de la même manière – mais sous le capot, le code s'exécute à une vitesse de bas niveau.
Gains mesurables
Les benchmarks sur un appareil Android typique montrent les accélérations suivantes après une compilation avec AGP 9.2.0, R8 activé et la minification activée :
- Envoi/réception de canaux (Channel send/receive) : 2,01 × plus rapide
- Verrouillage/déverrouillage de mutex (Mutex lock/unlock) : 1,90 × plus rapide
- Mises à jour de
StateFlow: 2,02 × plus rapide - Dispatch des coroutines : 1,68 × plus rapide
Ces chiffres se traduisent par des améliorations tangibles de l'expérience utilisateur. Un lancement à froid qui passait une fraction de seconde à attendre la synchronisation des coroutines se termine désormais plus rapidement, laissant plus de marge au thread UI pour rendre la première image. Une moindre contention du CPU permet également au processeur de retourner plus rapidement en mode veille, ce qui peut améliorer l'autonomie de la batterie.
Comment en profiter
Aucune modification de code n'est requise. Pour activer la réécriture, vous avez besoin de :
- AGP 9.2.0 ou version ultérieure – la version qui contient la mise à jour de R8.
- R8 – utilisé automatiquement lorsque vous compilez avec l'AGP mentionné ci-dessus.
- Kotlin Coroutines 1.8.0+ – la version de la bibliothèque qui utilise le motif
AtomicFieldUpdaterattendu par l'optimiseur. isMinifyEnabled = truedans votre type de build de release – R8 ne s'exécute que lorsque la minification est activée.
La seule étape supplémentaire consiste à auditer vos règles ProGuard (ou R8). Les directives -keep trop larges qui préservent les champs volatiles ou les classes d'updater elles-mêmes bloquent la réécriture. Assurez-vous que vos règles permettent à R8 de modifier ces champs ; sinon, l'optimiseur reviendra à l'implémentation par réflexion d'origine.
Vous pouvez vérifier la transformation avec l'APK Analyzer d'Android Studio. Ouvrez l'APK compilé, localisez une classe de support des coroutines telle que JobSupport, et inspectez le bytecode décompilé. Si la réécriture a réussi, les champs d'updater statiques seront absents et vous verrez des appels directs à Unsafe à la place.
Avertissements et contre-arguments
L'optimisation repose sur deux conditions que tous les projets ne remplissent pas :
- La minification doit être activée. Les builds de debug, ou les builds de release qui désactivent la minification pour faciliter le débogage, ne bénéficieront pas de cette amélioration.
- Les règles ProGuard doivent être permissives. Les projets utilisant des patterns
-keepagressifs pour les composants internes des coroutines pourraient devoir assouplir ces règles, ce qui pourrait exposer les classes internes à des bugs liés au shrinking s'ils ne sont pas testés avec soin.
Il reste conseillé de tester sur la gamme d'appareils que vous ciblez.
Ce qu'il faut surveiller ensuite
Cette réécriture montre comment les transformations de bytecode au moment du build peuvent extraire des performances cachées derrière les abstractions du langage. Envisagez de profiler votre propre code intensif en coroutines pour confirmer les gains sur votre charge de travail spécifique.
À retenir : Passer à AGP 9.2.0 et activer la minification de R8 permet aux applications Kotlin intensives en coroutines de presque doubler les vitesses de synchronisation critiques, le tout sans toucher au code source — à condition que la configuration du build laisse l'optimiseur faire son travail.
