Ingénieur en performances Android
Les applications Android sont souvent livrées sous-optimisées en raison de règles de conservation trop conservatrices, souvent issues de bibliothèques tierces. Cela entraîne des APK volumineux, des démarrages à froid plus lents et des performances d'exécution dégradées.
Avec les nouveaux seuils de performances de Google Play pour l'optimisation DEX, la visibilité des applications dépend désormais directement de leur capacité à rester en avance sur la courbe d'optimisation.
Cette présentation introduit un workflow R8 moderne pour lutter contre l'accumulation de règles. Vous allez apprendre à effectuer les tâches suivantes :
Transcription
Hi, everyone!
Je m'appelle Breana et je fais partie de l'équipe DevRel Android.
Et je m'appelle Chris Craik. Je fais partie de l'équipe Android Performance.
Bienvenue dans les règles de conservation avec sûreté du typage et l'optimisation DEX.
Aujourd'hui, nous allons parler de l'optimisation DEX, de son importance et de quelques nouveautés intéressantes.
Pendant longtemps, l'optimisation DEX a été considérée comme une fonctionnalité intéressante, mais pas indispensable.
Vous l'avez peut-être configuré une fois, puis laissé tel quel, ou vous vous en êtes occupé quand vous avez eu un peu de temps.
Toutefois, l'industrie mobile étant confrontée à d'importantes contraintes d'approvisionnement en matériel qui affectent la disponibilité de la mémoire des appareils, un DEX bien optimisé est essentiel pour que votre application utilise moins de mémoire, démarre plus rapidement, réduise les erreurs ANR et améliore les performances d'affichage et d'exécution, de manière générale.
Pour maintenir un écosystème Android sain et s'assurer que vos applications continuent d'offrir des expériences de qualité, Google Play a annoncé une nouvelle exigence de qualité des applications avec des seuils de performances qui entreront en vigueur en février.
Cela inclut des seuils pour l'optimisation du code DEX.
Les applications doivent atteindre un taux supérieur à 25 % pour la minification, l'optimisation et l'obscurcissement.
Qu'est-ce que cela implique pour vous en tant que développeur Android ?
Un code DEX non optimisé a un impact négatif sur l'expérience utilisateur, mais il peut également avoir des conséquences sur la visibilité et les capacités de publication de votre application sur le Play Store.
Cependant, vous êtes au bon endroit. À la fin de cette présentation, vous disposerez des outils nécessaires pour non seulement répondre à ces exigences, mais aussi les dépasser.
L'optimisation du code DEX est possible avec R8, l'optimiseur d'applications d'Android.
Bien que R8 ne soit pas strictement obligatoire si un autre optimiseur d'application peut faire passer votre application au-dessus de ces seuils, il s'agit de la méthode recommandée, intégrée directement à la chaîne d'outils d'Android.
Même si un app bundle DEX bien optimisé utilise moins de mémoire, les principaux avantages sont toujours liés aux performances : moins d'erreurs ANR, un démarrage plus rapide et de meilleures performances d'exécution.
L'optimisation de votre application a des résultats positifs réels et prouvés pour votre application et vos utilisateurs.
Par exemple, en adoptant le mode complet de R8 et en affinant ses règles de conservation, Grab a pu optimiser son application de 78 %, la réduire de 79 % et l'obscurcir de 82 %.
En voyant cette diapositive, vous vous dites peut-être : "D'accord, ces chiffres sont assez élevés, mais qu'est-ce que cela signifie concrètement ?"
Grâce à ces améliorations, combinées aux profils de référence, l'entreprise a réduit les ANR de 32 % et amélioré le démarrage de 35 %.
Lorsque les applications les plus populaires adoptent R8, elles constatent une diminution moyenne de 38 % de la taille du DEX et de 16 % des ANR perçues par l'utilisateur.
Si vous utilisez déjà R8, nous avons une excellente nouvelle pour vous.
Pour les applications qui ont déjà activé R8 et qui prennent des mesures pour améliorer davantage les niveaux d'optimisation, de réduction et d'obscurcissement de plus de 50 %, on constate en moyenne une diminution supplémentaire de 25 % de la taille DEX, une diminution de 4 % du temps de démarrage et une diminution supplémentaire de 17 % des ANR perçues par l'utilisateur, en plus des 16 % que nous avons montrés dans la diapositive précédente.
Aujourd'hui, nous allons vous expliquer comment obtenir de tels résultats.
Nous allons aborder les outils qui peuvent vous aider à optimiser votre code DEX et à passer de règles de conservation trop larges et mal configurées à des règles spécifiques et précises.
Tout d'abord, nous tenons à préciser que R8 existe depuis de nombreuses années.
Nous sommes conscients que, même si elle permet une optimisation à fort impact, sa configuration nécessite encore beaucoup d'attention.
Les appels réfléchis signifient que les entrées sont imprévisibles et peuvent entraîner des plantages au moment de l'exécution.
Par le passé, cela a incité les équipes chargées des applications et des bibliothèques à ajouter des règles de conservation trop larges, juste pour ne pas prendre de risques.
Nous savons que 41 % des meilleures applications sont en dessous du niveau d'optimisation de 25 % défini par Google Play, mais nous savons également que 13 % des meilleures applications atteignent déjà trois fois ce seuil.
Et ce sont des applications de secteurs et de parcours utilisateur très variés qui parviennent à atteindre ce chiffre.
Vous pouvez obtenir des scores d'optimisation élevés pour votre application, à condition de disposer des outils et de la visibilité appropriés pour gérer vos propres règles de conservation et celles héritées des bibliothèques.
Avant de nous intéresser aux nouveaux outils et améliorations qui vous aideront à y parvenir, nous voulions également mettre en avant quelques autres améliorations apportées à R8 au cours de l'année écoulée, qui facilitent également cette optimisation.
Tout d'abord, les nouveaux modèles d'application incluent un champ d'application du package, ce qui permet de garantir un niveau d'optimisation minimal dès le départ.
L'analyseur de configuration R8, dont nous parlerons dans un instant, est désormais compatible avec les anciennes versions d'AGP.
Il est également compatible de manière non officielle avec Bazel.
Et dans AGP 9.2, les primitives de coroutine sont deux fois plus rapides.
Grâce aux améliorations apportées à R8 dans AGP 9.2, Tinder a constaté une diminution de 13 % des ANR après la mise à niveau depuis la version 9.0.1.
AGP 9.3 propose également une API R8 repensée avec des noms plus clairs, des valeurs par défaut plus simples pour les règles par défaut et la réduction des ressources, ainsi que la prise en charge du répertoire source AGP pour les règles de conservation.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}Les nouveautés que nous allons aborder aujourd'hui améliorent également l'expérience des développeurs.
Nous allons commencer par voir comment utiliser l'analyseur de configuration R8 et la compétence R8 pour identifier les règles obsolètes qui ralentissent le processus et déterminer comment les améliorer.
Ensuite, Chris expliquera comment passer de règles de conservation larges et génériques aux nouvelles annotations précises, de type sécurisé et générées automatiquement.
Enfin, nous verrons comment utiliser les nouveaux outils de validation de la CLI pour garantir une optimisation agressive sans provoquer de plantages.
L'analyseur de configuration R8 est donc l'outil idéal pour commencer à examiner l'optimisation DEX.
Si vous n'avez pas encore utilisé l'outil, voici à quoi il ressemble.
Il s'agit d'un rapport HTML qui affiche des informations sur votre configuration R8.
Ce rapport concerne mon application, qui inclut une bibliothèque appelée Analytics Bloat.
Comme vous pouvez le deviner, il ne s'agit pas d'une vraie application ni d'une vraie bibliothèque, mais elle est mise en évidence en bleu en haut.
L'analyseur de configuration mesure les éléments suivants : la minification, qui correspond au pourcentage de classes, de méthodes et de champs que R8 n'est pas empêché de minifier par les règles de conservation.
Cette valeur est calculée après la première passe de réduction de R8.
Il mesure également l'optimisation, qui correspond au pourcentage de code ouvert aux optimisations de l'ensemble du programme, telles que l'intégration de méthodes et la fusion de classes.
Et l'obfuscation, qui correspond au pourcentage de membres pouvant être obfusqués ou renommés avec des identifiants plus courts.
Je tiens également à préciser que, comme l'analyseur de configuration R8 calcule ces scores avant les optimisations finales et la compilation DEX, les valeurs que vous voyez dans ce rapport pour l'optimisation, la réduction et l'obscurcissement ne correspondront pas exactement à celles que vous voyez dans la Play Console.
En effet, Play mesure la sortie DEX finale.
Cette différence permet de s'assurer que, lors du développement, vous pouvez accéder à une boucle de rétroaction locale rapide.
Toutefois, les scores que vous voyez dans l'analyseur de configuration R8 sont toujours très utiles pour valider le potentiel d'optimisation DEX avant d'importer votre application sur le Play Store.
Le rapport inclut également le tableau "Analyse des règles de conservation", qui est la vue principale qui s'affiche lorsque vous accédez au rapport.
Vous pouvez ensuite cliquer sur n'importe quelle règle pour voir le nombre exact de classes, de méthodes et de champs qu'elle conserve. Vous pouvez ainsi identifier les règles qui conservent plus d'éléments que nécessaire.
Elle comporte également des règles subsumées, qui sont mises en évidence ici.
Il s'agit de règles qui se chevauchent avec une règle plus large.
Ils sont mis en évidence pour que vous puissiez supprimer le plus large et ne conserver que le plus précis.
Il vous montre également les règles inutilisées, c'est-à-dire celles qui peuvent être supprimées sans risque pour la stabilité de votre application.
Config Analyzer a donc été conçu pour résoudre quelques problèmes, à commencer par l'accumulation d'anciennes règles.
Auparavant, sans visibilité par règle, il était risqué de supprimer d'anciennes règles.
Les équipes avaient donc tendance à les laisser de côté ou à ajouter des caractères génériques plus larges.
Deuxièmement, cela vous aide à identifier les angles morts des règles de la bibliothèque.
Par conséquent, lorsque vous ajoutez un SDK, il est difficile de savoir si une règle fournie avec cette bibliothèque est trop large ou si elle est même nécessaire, par exemple, 10 versions plus tard.
Nous reviendrons sur les bibliothèques dans un instant.
Troisièmement, il vous aide à évaluer l'impact de l'optimisation.
Auparavant, il n'était pas facile de savoir si une règle conservait deux classes ou 2 000.
Voyons comment R8 fonctionne dans le contexte de mon application et de la bibliothèque que nous avons examinée précédemment.
Ici, j'ai un module d'application et trois bibliothèques.
Et portez une attention particulière à la bibliothèque trois sur la droite.
Il s'agit de la bibliothèque Analytics Bloat, qui inclut une règle de conservation à l'échelle du package.
Tout d'abord, R8 identifie le point d'entrée racine à partir de toutes les règles de conservation fusionnées, qui sont mises en surbrillance en jaune ici.
En général, il s'agit des principaux points d'entrée de l'application, comme les classes du fichier manifeste.
De plus, en raison de la règle de conservation avec caractère générique de la bibliothèque Analytics Bloat, chaque classe de la bibliothèque est traitée comme un point d'entrée.
Ensuite, R8 trace l'accessibilité dans le graphique d'appel à partir de ces racines.
Toute classe accessible à partir d'une racine est marquée en vert pour être conservée, tandis que les classes non référencées sont classées comme code mort et sont marquées en rouge ici.
R8 effectue ensuite un tree shaking et supprime le code mort non référencé.
Voici ce qu'il reste après cette première passe de réduction.
Notre application a un score de réduction de 42,5 %, ce qui est supérieur au seuil de 25 % de Google Play, mais ce n'est pas encore tout à fait ce que nous souhaitons.
Cela est principalement dû à la règle de conservation à l'échelle du package de la bibliothèque Analytics.
Cela signifie que seules 17 des 40 classes restantes peuvent être réduites davantage.
Ce que je veux dire ici, c'est qu'une seule règle de conservation incorrecte peut entraîner des problèmes majeurs pour l'optimisation de votre application.
En gardant cela à l'esprit, concentrons-nous rapidement sur les bibliothèques.
L'année dernière, notre équipe a beaucoup investi pour comprendre l'impact des règles de conservation des consommateurs de bibliothèques sur l'optimisation de l'écosystème.
De nombreuses bibliothèques sont fournies avec des règles trop larges au nom de la stabilité.
Ils peuvent également spécifier des règles générales dans leur documentation pour que les développeurs d'applications les ajoutent eux-mêmes à leur application.
Toutefois, il n'est pas nécessaire de publier des règles générales pour assurer la stabilité, et cela a un impact négatif sur les performances, dont les coûts sont principalement à votre charge en tant que développeur d'applications.
Nous avons vu des cas où l'intégralité de la bibliothèque est conservée par une règle de conservation à l'échelle du package, comme la bibliothèque d'analyse "bloat". Dans le cas de cette bibliothèque, cela a eu des conséquences importantes sur le potentiel d'optimisation de mon application.
Je tiens également à préciser que même nos propres SDK chez Google n'étaient pas à l'abri de ce problème, et que plus d'une douzaine de bibliothèques Google ont corrigé des règles de conservation trop larges au cours de l'année écoulée.
Si vous souhaitez utiliser l'analyseur de configuration, il est intégré directement à AGP 9.3, mais il est également disponible pour les versions antérieures si vous mettez à niveau votre version de R8.
L'exécution de cette commande génère le rapport HTML que nous avons présenté, qui affiche vos règles de conservation, ainsi que l'impact sur la réduction, l'optimisation et l'obscurcissement pour les règles individuelles et votre application dans son ensemble.
Si vous exécutez un build de version R8 complet, le rapport est généré automatiquement et se trouve à ce chemin d'accès.
L'avantage de cette tâche autonome est qu'elle prend moins de temps à générer que la compilation complète.
L'équipe de WHOOP, qui est peut-être présente aujourd'hui, a exécuté l'analyseur de configuration et a obtenu 74 % pour la réduction, 74 % pour l'obscurcissement et 0 % pour l'optimisation.
Comme vous pouvez l'imaginer, l'équipe a été très surprise de voir cette valeur d'optimisation.
La visibilité offerte par l'analyseur de configuration leur a permis de découvrir que leur build utilisait un ancien fichier ProGuard qui désactivait l'optimisation. En passant à la version mise à jour, ils ont pu activer l'optimisation pour leur application.
Cela a permis d'optimiser l'APK de 55 % et de réduire sa taille de 4 %.
En les combinant avec les profils de référence, l'entreprise a amélioré les temps de démarrage à froid de 38 % et réduit de 28 % le nombre d'images figées excessives.
L'écran de gauche n'utilise pas de profils de référence, tandis que celui de droite en utilise. Comme vous pouvez le voir, le démarrage de l'application est plus rapide avec les profils de référence.
Laissez-moi vous montrer cela une fois de plus.
Parfait.
Ce lien vous redirigera vers le guide de l'analyseur de configuration. L'examen manuel du rapport est une bonne chose, mais comme vous pouvez l'imaginer, cela peut prendre beaucoup de temps en fonction de l'état de la configuration R8 de votre application.
Pour accélérer les choses, nous disposons d'une compétence d'analyseur R8.
$ android skills list
$ android skills add r8-analyzerVous pouvez demander à l'agent d'analyser votre configuration R8. Non seulement la compétence génère le rapport de l'analyseur de configuration R8, mais elle vous indique également les règles les plus importantes.
Voici un exemple de résultat. Pour souligner quelques détails, il indique un score d'optimisation de 42 %, ce qui signifie que 58 % de la codebase ne peut pas être optimisée par R8.
Si nous examinons spécifiquement la section des règles de conservation, la règle de conservation à l'échelle du package pour la bibliothèque de gonflement des analyses empêche l'optimisation de 22 % du codebase. La compétence nous recommande également d'affiner cette règle de conservation, car, comme nous le savons déjà, elle est trop large.
Vous pouvez désormais diagnostiquer les causes de l'augmentation de la taille de l'application et du blocage de l'optimisation DEX à l'aide de l'analyseur de configuration R8 et de la compétence.
Mais une fois que vous avez trouvé une règle de conservation trop large, comment la remplacer en toute sécurité sans provoquer de plantage inattendu ?
Je vais laisser la parole à Chris, qui va vous montrer comment passer des anciennes règles de conservation aux nouvelles annotations de conservation générées automatiquement et de type sécurisé.
Merci, Breana.
Voyons comment nous en sommes arrivés là.
Avec cette bibliothèque d'analyse surchargée et toutes ces règles de conservation, ou plutôt cette règle de conservation qui conserve pratiquement tout, le principal problème auquel nous sommes confrontés est la réflexion.
Examinons un peu de code de réflexion, juste pour rappel.
fun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}Il s'agit d'une fonction d'instanciation d'objet qui gonfle un objet particulier, et ce de manière réfléchie.
L'inconvénient, dans le contexte de l'optimisation DEX, est que tous ces appels statiques sont retardés jusqu'à l'exécution.
En examinant simplement le code, selon l'origine de ce nom de classe, notre optimiseur R8 n'est pas en mesure de voir ce qui se passe ici.
Ce sont les situations qui interrompent l'optimisation Dex par défaut. Vous devez donc généralement ajouter une règle de conservation.
Idéalement, vous devriez éviter cela, n'est-ce pas ?
Idéalement, nous devrions éviter complètement la réflexion, surtout maintenant en 2026, où nous disposons d'outils de génération de code incroyables pour la sérialisation et l'injection de dépendances, qui peuvent éliminer complètement cette catégorie de problèmes.
Mais ce n'est pas tout, n'est-ce pas ?
La réflexion reste nécessaire dans certains cas.
Voyons donc comment cela s'est produit dans cette bibliothèque surchargée.
Voici un exemple courant : une dépendance facultative.
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}Ici, nous gonflons un type particulier qui n'est pas accessible dans le chemin de classe de la bibliothèque. Nous devons donc le faire avec une chaîne.
Il s'agit d'une référence à cette classe particulière qui peut être présente ou non au moment de la compilation de la bibliothèque. Il est donc tout à fait raisonnable que cette bibliothèque utilise la réflexion pour trouver un membre dans une bibliothèque associée.
Un autre cas d'utilisation est l'instanciation dynamique de sous-classes.
Ainsi, en particulier s'il s'agit d'un élément provenant d'un fichier manifeste ou d'un élément envoyé depuis un serveur, nous informons l'application locale : "Hé, s'il te plaît, gonfle cet élément, crée-le tardivement."
La troisième catégorie que nous voyons ici est la sérialisation embarrassante.
En effet, une sérialisation JSON est effectuée en itérant sur tous les membres.
C'est absolument dégoûtant, mais ce genre de choses persiste parfois.
Il n'est pas toujours corrigé au moment où il le faudrait.
Mais parlons du résultat de cette opération.
À mesure que ces appels de réflexion s'accumulent, vous êtes de plus en plus tenté de choisir la solution de facilité et d'optimiser l'expérience des développeurs plutôt que celle des utilisateurs.
C'est un peu embêtant, mais je vais ajouter une tâche et tout ira bien.
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };Un jour, je m'en occuperai et je le réparerai, n'est-ce pas ?
Voyons donc ce que nous sommes censés faire, ou du moins ce que nous étions censés faire avant aujourd'hui, et comment vous écririez une règle de conservation idéale ici.
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}Ok, attachez votre ceinture.
La première partie consiste à conserver la classe particulière que nous renvoyons à partir de cette fonction réfléchissante.
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}En fait, non, attendez.
Tu sais quoi ?
Ce n'est pas correct.
Il s'agit d'une classe Kotlin. Je dois m'assurer qu'elle est de type chaîne. Je vais donc ajouter l'extension .kt.
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}En fait, l'important ici est que je ne renvoie pas réellement un EventFilter, mais une sous-classe de EventFilter. Je dois donc conserver les classes qui étendent EventFilter pour pouvoir l'augmenter correctement.
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}Et si je veux le faire plus efficacement, cela aurait fonctionné, mais pour le rendre plus efficace, en tant que bon développeur de bibliothèque, que devrais-je faire de plus ?
Je devrais plutôt dire -keepclasseswithmembers, et ne conserver que les classes qui ont ce constructeur <init> particulier que j'utilise pour gonfler l'objet.
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}Et si je veux être vraiment sympa, j'ajouterai allowaccessmodification pour permettre une optimisation encore plus poussée de toutes les API ciblées par cette règle Keep, car je ne veux pas empêcher une optimisation plus poussée de R8. Je veux essayer d'être un bon citoyen.
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}Enfin, la dernière étape, que presque personne ne fait, car elle demande un peu plus de travail, mais qui est idéale pour une bibliothèque, consiste à la rendre conditionnelle.
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}En théorie, vous n'avez pas besoin de conserver toutes ces sous-classes, sauf si vous appelez réellement cette fonction. C'est ce que fait cette instruction "if" en haut d'une règle Keep.
Je pourrais vous demander de lever la main, mais je sais que presque personne ne le fait, n'est-ce pas ?
C'est-à-dire l'écrire et la rendre conditionnelle.
Et si nous pouvions améliorer un peu tout ça ?
Voyons d'abord ce que ça donne.
Il existe peut-être une autre façon de procéder.
Nous avons vu ce qui se passe si j'écris une règle Keep manuelle et étroite.
C'est possible techniquement, mais c'est difficile à écrire, à lire et à gérer, car il y a beaucoup de petits éléments de type chaîne qui peuvent facilement être endommagés en cas de problème sous-jacent.
En dessous, quelque chose change.
Et nous avons des alternatives.
Nous avons des annotations comme @Keep.
@Keep est présent dans le package d'annotations AndroidX depuis longtemps, mais son utilisation est désormais déconseillée.
@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }Ce n'est pas une bonne solution pour les développeurs de bibliothèques. En fait, elle est en quelque sorte interdite en interne, car même si elle est plus facile à gérer, elle est très rigide et ne propose pas certaines des optimisations dont je parlais précédemment.
En quelque sorte, non.
Il ne permet pas d'analyser le site d'appel.
Reprenons l'exemple précédent : "Je peux simplement ajouter @Keep à mon EventFilter, n'est-ce pas ?"
@Keep
class EventFilter { … }Eh non.
@Keep ne cible pas les sous-classes, mais uniquement cette classe.
@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }En fait, je dois cibler toutes les sous-classes, et j'écris maintenant plus d'annotations @Keep que je ne l'aurais fait avec cette seule règle Keep qui était difficile à écrire.
Ces deux approches présentent donc des problèmes, n'est-ce pas ?
On les oublie facilement, on les découvre tardivement et ils ne sont pas conditionnels à l'appelant par défaut.
Nous voulons une meilleure solution, en particulier pour les bibliothèques.
Nous voulons qu'il soit plus facile d'obtenir des résultats dès le départ.
L'un des principaux problèmes que nous constatons ici est que ces éléments sont éloignés de la réflexion contre laquelle nous nous protégeons.
En fin de compte, la solution que nous voulons est quelque chose qui se trouve juste à côté du code de réflexion malveillant qui gonfle les éléments ciblés.
Parlons donc de Jetpack Keep, la nouvelle bibliothèque et le nouveau plug-in d'AndroidX conçus pour résoudre précisément ce problème.
L'objectif de Jetpack Keep est de commencer par générer des règles de conservation à partir d'annotations, mais aussi de générer des annotations directement à partir de la réflexion déjà présente dans votre codebase.
La configuration est donc très simple.
plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}Il s'agit d'un plug-in Gradle et d'une bibliothèque d'annotations.
Mais voyons comment cela s'exécute au moment de la compilation.
Si je reprends l'exemple précédent où je gonfle une sous-classe d'un type connu, que dois-je faire ?
J'exécute simplement lintFix.
./gradlew analyticsbloat:lintFixLint analyse ici toute la réflexion qui se produit dans cette méthode et indique que l'annotation est nécessaire.
@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}Cette annotation exprime exactement ce que fait la réflexion dans cet appel.
Il indique : "Je gonfle une classe de ce type."
Dans ce cas particulier, EventFilter est une classe connue.
Je n'utilise pas de chaîne pour y faire référence, car ce n'est pas nécessaire.
Cela ajoute une sécurité supplémentaire, et j'encode même le parameterTypes pour qu'il indique exactement quel constructeur est conservé.
Mais à quoi cela sert-il ?
Par exemple, comment cela influence-t-il une éventuelle compilation R8 ?
Il génère la règle de conservation. Comme vous l'avez vu, il s'agit exactement de la même règle de conservation idéale que celle que nous avions précédemment.
# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}Il cible exactement les mêmes classes, les sous-classes, et il est conditionné par ce point de réflexion.
Et je n'ai pas besoin de gérer… Je n'ai pas besoin de me soucier d'annoter toutes les sous-classes.
Je n'ai pas besoin de conserver un fichier de règles de conservation distinct ailleurs.
Cette méthode est beaucoup plus simple pour gérer vos règles de conservation.
Revenons à l'exemple de dépendance facultative.
@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}Ici, la classe n'était pas dans le chemin de classe, mais je peux quand même la référencer.
Je le fais simplement en utilisant une chaîne, mais tout le reste, les types de paramètres, peuvent être référencés par des références de classe, donc je le fais.
Encore une fois, nous générons la règle de conservation idéale pour ce cas.
# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}Elle est conditionnelle, spécifique et ciblée, et permet d'optimiser les classes ciblées dans la mesure du possible.
Voici la réflexion sur les champs. Dans ce cas particulier, j'itère sur tous les champs d'un objet, mais là encore, nous gérons cela très bien en indiquant de cibler tous les champs avec cet opérateur étoile.
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}Cela me permet de cibler tous les champs. Comme vous pouvez le voir, cela génère la règle de conservation exacte que vous attendez pour cette réflexion incroyablement malheureuse qui ne devrait pas se trouver dans mon application. Mais je reviendrai et je nettoierai tout ça, je vous le promets.
# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}Alors, que fait Jetpack Keep ?
Il réduit considérablement la maintenance des règles de conservation.
Pour tout ce que vous pouvez générer automatiquement, cela vous évite d'avoir à vous en préoccuper et de risquer de ne pas respecter les règles de conservation.
Il vous offre également le déterminisme de lint.
Il ne se contente pas de corriger les problèmes au fur et à mesure que vous effectuez des itérations et que vous essayez de migrer. Il détecte également les nouvelles réflexions et les modifications de réflexion chaque fois que le code change, et cette sécurité est absolument essentielle.
Puis-je l'utiliser ?
Quelles sont les dépendances ici ?
Le plug-in et lint nécessitent AGP 8.7.
Cela devrait être le cas de presque tout le monde.
Mais il est également très flexible quant à l'endroit où vous pouvez l'utiliser.
Vous pouvez l'utiliser dans les applications.
Vous pouvez l'utiliser dans les bibliothèques Android, les AAR, les bibliothèques KMP et les JAR (avec un peu de câblage manuel dans ce cas).
Mais la principale caractéristique ici est que cette méthode peut être utilisée en toute sécurité par un développeur de bibliothèque.
Même si quelqu'un utilise une ancienne version de R8 ou même un outil tiers qui consomme votre bibliothèque, nous exportons toujours les règles de consommation standard réelles afin que tout le monde puisse toujours vous utiliser et n'ait pas à se soucier de consommer une sorte de nouveau format incompatible.
Maintenant que j'ai expliqué comment Jetpack Keep peut vous aider à optimiser votre code et à vous soucier moins de la protection de vos appels de code réfléchis, parlons des nouvelles fonctionnalités que nous avons récemment lancées dans la CLI Android pour vérifier l'étape de la dernière version.
Breana a mentionné précédemment que les résultats Play vous fournissent des informations sur la taille de votre DEX et vos pourcentages d'optimisation.
Nous avons commencé à le rendre disponible via la CLI Android.
Il utilise exactement la même logique que le Play Store. Vous pouvez donc analyser votre bundle directement dans la CLI, sans avoir à l'importer sur le Play Store.
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>Et vous obtenez exactement les mêmes informations directement et immédiatement grâce à l'analyse locale.
Dans ce cas précis, l'application reçoit un avertissement indiquant que son taux d'optimisation est très faible, ainsi que des informations sur la taille de son fichier DEX et ses pourcentages d'optimisation, exactement comme le ferait le Play Store.
Nous avons également une sortie JSON. Si vous souhaitez écrire un script pour automatiser les tests de régression par rapport à l'une de ces valeurs qui diminue considérablement, car, encore une fois, ce sont les chiffres que Play applique, vous devez les suivre. Vous pouvez le faire très facilement avec cette valeur JSON, ces rapports JSON.
{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}Ici, nous voyons que les mêmes chiffres sont également présents.
Ce n'est pas la seule fonctionnalité de android analyze.
android analyze peut également s'exécuter sur des bibliothèques.
$ android analyze <library.aar>
$ android analyze <library.jar>Comme l'a mentionné Breana, de nombreux problèmes sont liés aux bibliothèques qui sont livrées avec des règles de conservation malheureuses.
Utilisons-le pour examiner certains de ces problèmes.
Ainsi, lorsque j'exécute cette commande sur un AAR particulier, je peux voir que cette bibliothèque présente un certain nombre de problèmes, qui sont mis en évidence tout en haut.
Cette bibliothèque présente un ratio de conservation élevé, ce qui signifie qu'elle conserve trop d'éléments avec des règles de conservation non limitées.
Il contient des règles de conservation générales qui l'empêchent d'être optimisé, et certains chiffres ci-dessous vous indiquent exactement à quel point cela est problématique. Il contient également des règles de conservation qui, et c'est peut-être le plus effrayant, ne ciblent rien, ce qui signifie probablement qu'elles ciblent quelqu'un d'autre de manière illimitée.
"Code smells" très dangereux.
Vous pouvez donc vérifier tout cela dans un AAR, en particulier en tant qu'éditeur de bibliothèque. C'est un excellent moyen de vous assurer que vous ne livrez pas de mauvaises règles de conservation et que vous ne les laissez pas s'infiltrer lorsque quelqu'un qui n'est peut-être pas aussi expert en écriture de règles de conservation travaille sur la bibliothèque.
Ici, vous pouvez voir que nous générons également un ratio de conservation.
C'est un bon moyen d'évaluer la qualité globale de votre bibliothèque.
Si j'optimise une bibliothèque individuelle de manière totalement autonome, sans aucune référence, avec R8, je devrais voir qu'elle fait zéro octet, n'est-ce pas ?
Il devrait être pratiquement vide, car il ne contient rien.
Ce serait l'idéal.
Dans ce cas, ce n'est pas le cas.
Vous pouvez voir directement la taille de R8 et D8.
En fait, nous mettons également en évidence certaines des règles de conservation les plus mauvaises pour les rendre un peu plus visibles.
Il est également au format JSON. Ainsi, que vous soyez un développeur d'applications qui essaie de contrôler les mises à jour de bibliothèque ou un développeur de bibliothèque qui essaie d'éviter des régressions importantes dans la qualité de votre bibliothèque, vous pouvez effectuer la même analyse et éviter ces problèmes grâce à un script simple qui encapsule la CLI Android.
{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}Si vous rencontrez un problème avec une bibliothèque, essayez d'abord de la mettre à jour.
Comme Breana l'a mentionné précédemment, nous avons corrigé plus d'une douzaine de bibliothèques Google pour corriger des règles de conservation très malheureuses. D'autres bibliothèques ont été corrigées, mais elles ne sont pas mises à jour par les applications qui les utilisent.
C'est un excellent point de départ.
Ensuite, vous pouvez rechercher un problème et contacter le développeur de la bibliothèque, et éventuellement joindre un rapport d'analyse qui lui indique : "Hé, vous conservez trop de données.
Aidez-moi à optimiser mon application."
Pour résumer, nous avons parlé de l'analyseur de configuration R8, qui vous aide à examiner tout ce qui se passe dans votre build R8, tout ce qui indique ce qui est optimisable et ce qui ne l'est pas.
Nous avons parlé de Jetpack Keep, la nouvelle bibliothèque qui vous aide à écrire automatiquement des règles de conservation sans lever le petit doigt.
Et Android CLI, le nouvel outil qui vous aide à vérifier et à vous assurer, en effectuant une double vérification par rapport à la dernière étape, que vous, votre agent ou l'ingénieur junior de votre équipe n'avez rien gâché pour vos clients.
Pour en savoir plus, consultez la documentation sur R8, l'analyseur de configuration et Jetpack Keep.
La documentation sur la CLI Android décrit la commande "analyze" et son utilisation.
Merci beaucoup.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}$ android skills list
$ android skills add r8-analyzerfun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# TODO: clean this up
-keep com.example.analytics.bloat.** { * };fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }@Keep
class EventFilter { … }@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}./gradlew analyticsbloat:lintFix@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}$ android analyze <library.aar>
$ android analyze <library.jar>{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}