Le système léger Android Go a des exigences accrues pour les applications préinstallées - la taille et la mémoire utilisées. Nous avons été confrontés au défi de répondre à ces exigences. Nous avons effectué un certain nombre d'optimisations et décidé de changer sérieusement l'architecture de notre shell graphique - Yandex.Luncher. Le chef de l'équipe de développement d'applications mobiles Alexander Starchenko a partagé cette expérience.
- Je m'appelle Alexander, je suis de Saint-Pétersbourg, de l'équipe qui développe Yandex.Loncher et Yandex.Phone. Aujourd'hui, je vais vous expliquer comment nous avons optimisé la mémoire dans Launcher. Tout d'abord, je vais expliquer brièvement ce qu'est le lanceur. Ensuite, nous discutons des raisons pour lesquelles nous devons optimiser la mémoire. Après cela, nous verrons comment mesurer correctement la mémoire et en quoi elle consiste. Passons ensuite à la pratique. Je vais parler de la façon dont nous avons optimisé la mémoire dans Launcher et comment nous sommes parvenus à une solution radicale au problème. Et à la fin, je vais parler de la façon dont nous surveillons l'utilisation de la mémoire, comment nous la gardons sous contrôle.

"Lanceur" ou "Lanceur" - pas si important. Chez Yandex, nous l'appelions Lanceur, et dans le rapport j'utiliserai le mot "Lanceur".

Autre point important: Le lanceur est assez largement distribué via des préréglages, c'est-à -dire que lorsque vous achetez un nouveau téléphone, Yandex.Loncher peut très souvent se révéler être le seul et unique gestionnaire d'applications, le gestionnaire de bureau à domicile sur votre téléphone.
Maintenant, pour les raisons pour lesquelles nous devons optimiser la mémoire. Je vais commencer par notre raison. En bref, c'est Android Go. Et maintenant plus longtemps. Fin 2017, Google a présenté Android Oreo et sa version spéciale, l'édition Android Oreo Go. En quoi est-il spécial? Cette version est conçue pour les bas de gamme, pour les téléphones bon marché avec jusqu'à un gigaoctet de RAM. Quoi d'autre est-elle spéciale? Pour les applications préinstallées sur cette version d'Android, Google propose des exigences supplémentaires. En particulier - les exigences pour la consommation de RAM. En gros, quelque temps après le lancement, la mémoire de l'application est supprimée et la taille ne doit pas dépasser 30 à 50 mégaoctets pour Launcher, selon la taille de l'écran du téléphone. 30 sur les plus petits, 50 sur les grands écrans.
Il convient également de noter que Google continue de développer ce domaine, et il existe déjà une édition Android Pie Go.
Quelles autres raisons pourrait-il y avoir pour optimiser l'utilisation de la mémoire? Tout d'abord, votre application sera moins susceptible de télécharger. Deuxièmement, il fonctionnera plus rapidement, car il sera moins susceptible de fonctionner sur le garbage collector et la mémoire sera allouée moins souvent. Les objets supplémentaires ne seront pas créés, les vues supplémentaires ne seront pas gonflées, etc. Indirectement, à en juger par notre expérience, cela entraînera une diminution de la taille apk de votre application. Tout cela ensemble vous fournira plus d'installations et de meilleures notes sur Google Play.
Ok, maintenant nous savons pourquoi optimiser la mémoire. Voyons par quels moyens la mesurer et en quoi elle consiste.

Beaucoup d'entre vous ont probablement vu cette photo. Il s'agit d'une capture d'écran du profil Android Studio, à partir d'une vue de la mémoire. Cet outil est décrit de manière suffisamment détaillée sur developer.android.com. Beaucoup d'entre vous les ont probablement utilisés. Qui n'a pas utilisé - essayez.
Qu'est-ce qui est bon ici? C'est toujours à portée de main. Il est pratique à utiliser dans le processus de développement. Cependant, il présente certains inconvénients. Toutes les allocations de votre candidature ne sont pas visibles ici. Par exemple, les polices téléchargées ne sont pas visibles ici. De plus, avec l'aide de cet outil, il n'est pas pratique de voir quelles classes sont chargées en mémoire, et vous ne pouvez pas utiliser cet outil en mode automatique, c'est-à -dire que vous ne pouvez pas configurer une sorte de test automatique basé sur le profil Android Studio.

L'outil suivant existe depuis le développement d'Android dans Eclipse, c'est Memory Analyzer, MAT, pour faire court. Il est fourni en tant qu'application autonome et est compatible avec les vidages de mémoire que vous pouvez enregistrer à partir d'Android Studio.
Pour ce faire, vous devrez utiliser un petit utilitaire, un convertisseur professionnel. Il est livré avec l'édition Android Go et présente plusieurs avantages. Par exemple, il peut créer des chemins vers les racines gs. Cela nous a beaucoup aidés à voir exactement quelles classes sont chargées par Launcher et quand elles sont chargées. Nous n'avons pas pu le faire à l'aide d'Android Studio Profiler.

L'outil suivant est l'utilitaire dumpsys, en particulier dumpsys meminfo. Vous voyez ici une partie de la sortie de cette commande. Il fournit une connaissance assez élevée de la consommation de mémoire. Cependant, il présente certains avantages. Il est pratique à utiliser en mode automatique. Vous pouvez facilement configurer des tests qui appellent simplement cette commande. Il montre également la mémoire immédiatement pour tous les processus. Et montre tous les emplacements. À notre connaissance, Google utilise la valeur de la mémoire de cet outil dans le processus de test.
Prenons un exemple de sortie pour décrire brièvement en quoi consiste la mémoire d'application. Le premier est Java Heap, tous les emplacements de votre code Java et Kotlin. Habituellement, cette section est suffisamment grande. Ensuite, le tas natif. Voici les allocations du code natif. Même si vous n'utilisez pas explicitement le code natif dans votre application, des allocations seront présentes ici, car de nombreux objets Android - la même vue - allouent de la mémoire native. La section suivante est Code. Tout ce qui concerne le code arrive ici: bytecode, polices. Le code peut également être assez volumineux si vous utilisez de nombreuses bibliothèques tierces non optimisées. Voici la pile logicielle de Java et de code natif, généralement de petite taille. Vient ensuite la mémoire graphique. Cela inclut Surface, les textures, c'est-à -dire la mémoire qui se propage entre le CPU et le GPU est utilisée pour le rendu. Vient ensuite la section Private Other. Cela inclut tout ce qui n'est pas tombé dans les sections ci-dessus, tout ce que le système n'a pas pu disperser sur eux. Il s'agit généralement d'une sorte d'allocations natives. Vient ensuite la section Système, c'est la partie de la mémoire système qui est attribuée à votre application.
Et à la fin, nous avons TOTAL, c'est la somme de toutes les sections répertoriées. Nous voulons le réduire.

Que faut-il savoir d'autre sur la mesure de la mémoire? Tout d'abord, notre application ne contrôle pas entièrement toutes les allocations. Autrement dit, nous, en tant que développeurs, n'avons pas le plein contrôle sur le code qui sera téléchargé.
Ce qui suit. La mémoire de l'application peut beaucoup sauter. Pendant le processus de mesure, vous pouvez observer de fortes différences dans les lectures. Cela peut être dû au temps nécessaire ainsi qu'à divers scénarios. À cet égard, lorsque nous optimisons la mémoire, l'analysons, il est très important de le faire dans les mêmes conditions. Idéalement, sur le même appareil. Encore mieux si vous avez la possibilité d'appeler le garbage collector.
Super. Nous savons pourquoi nous devons optimiser la mémoire, comment la mesurer correctement, en quoi elle consiste. Passons à la pratique, et je vais vous dire comment nous avons optimisé la mémoire dans Launcher.

Telle était la situation au début. Nous avions trois processus, qui allouaient au total environ 120 mégaoctets. C'est presque quatre fois plus que ce que nous aimerions recevoir.

En ce qui concerne l'allocation du processus principal, il y avait une grande section de tas Java, beaucoup de graphiques, de gros code et un tas natif assez grand.

Tout d'abord, nous avons abordé le problème assez naïvement et avons décidé de suivre certaines recommandations de Google à partir de certaines ressources, d'essayer de résoudre le problème rapidement. Nous avons attiré l'attention sur les méthodes synthétiques générées au cours du processus de compilation. Nous en avions plus de 2 000. En quelques heures, nous les avons tous supprimés, nous avons supprimé la mémoire.

Et ils ont obtenu un gain d'environ un ou deux mégaoctets dans la section de code. Super.
Ensuite, nous avons tourné notre attention vers l'énumération. Comme vous le savez, l'énumération est une classe. Et comme Google l'a finalement admis, l'énumération n'est pas très efficace en mémoire. Nous avons traduit toutes les énumérations en InDef et StringDef. Ici, vous pouvez m'objecter que ProgArt vous aidera ici. Mais en fait, ProgArt ne remplacera pas tous les énumérations par des types primitifs. Il vaut mieux le faire vous-même. Au fait, nous avions plus de 90 enum, pas mal.

Cette optimisation a déjà pris des jours, car la plupart devaient être faites manuellement, et nous avons gagné environ trois à six mégaoctets dans la section tas de Java.
Ensuite, nous avons attiré l'attention sur la collection. Nous avons utilisé des collections Java assez standard, telles que HashMap. Nous en avions plus de 150, et tous ont été créés au début de Launcher. Nous les avons remplacés par SparseArray, SimpleArrayMap et ArrayMap et avons commencé à créer des collections avec une taille prédéterminée afin que les emplacements vides ne soient pas alloués. Autrement dit, nous transmettons la taille de la collection au constructeur.

Cela a également donné un certain gain, et cette optimisation nous a également pris des jours, dont la plupart nous l'avons fait manuellement.
Ensuite, nous avons pris une mesure plus spécifique. Nous avons vu que nous avons trois processus. Comme nous le savons, même un processus vide dans Android prend environ 8 à 10 mégaoctets de mémoire, beaucoup.
Mon collègue Arthur Vasilov a donné des détails sur les processus. Il n'y a pas si longtemps, lors de la conférence Mosdroid,
son rapport , également sur Android Go.

Qu'avons-nous eu après ces optimisations? Sur l'appareil de test principal, nous avons observé une consommation de mémoire de l'ordre de 80 à 100 mégaoctets, pas assez mauvaise, mais toujours pas suffisante. Nous avons commencé à mesurer la mémoire sur d'autres appareils. Nous avons constaté que sur des appareils plus rapides, la consommation de mémoire était beaucoup plus importante. Il s'est avéré que nous avions de nombreuses initialisations différentes en attente. Après un certain temps, Launcher a gonflé certaines vues, lancé certaines bibliothèques, etc.

Qu'avons-nous fait? Tout d'abord, nous avons parcouru la vue, toutes les dispositions. Suppression de toutes les vues gonflées avec une visibilité disparue. Ils les ont mis dans des dispositions distinctes, ont commencé à les gonfler par programme. Ceux dont nous n'avions pas besoin, nous arrêtions généralement de gonfler jusqu'au moment où l'utilisateur en avait besoin. Nous avons fait attention à l'optimisation de l'image. Nous avons arrêté de charger des images que l'utilisateur ne voit pas pour le moment. Dans le cas de Launcher, il s'agissait d'images-icônes d'applications dans la liste complète des applications. Jusqu'à son ouverture, nous ne les expédions pas. Cela nous a donné une très bonne victoire dans la section graphique.
Nous avons également vérifié nos caches d'images en mémoire. Il s'est avéré que toutes n'étaient pas optimales; toutes les images correspondant à l'écran du téléphone sur lequel Launcher était exécuté n'étaient pas stockées en mémoire.
Après cela, nous avons commencé à analyser la section de code et avons remarqué que nous avions beaucoup de classes assez lourdes quelque part. Il s'est avéré que ce sont principalement des classes de bibliothèque. Nous avons trouvé des choses étranges dans certaines bibliothèques. L'une des bibliothèques a créé HashMap et dans un initialiseur statique, il l'a obstrué avec un nombre suffisamment important d'objets.

Une autre bibliothèque a également chargé des fichiers audio dans un bloc statique, qui occupait environ 700 kilo-octets de mémoire.

Nous avons arrêté d'initialiser ces bibliothèques, nous avons commencé à travailler avec elles uniquement lorsque ces fonctions sont vraiment nécessaires aux utilisateurs. Toutes ces optimisations ont pris plusieurs semaines. Nous avons beaucoup testé, vérifié que nous n'avons pas introduit de problèmes supplémentaires. Mais nous avons également obtenu une assez bonne victoire, environ 25 sur 40 mégaoctets dans les sections Native, Heap, Code et Java Heap.
Mais cela ne suffisait pas. La consommation de mémoire n'est toujours pas tombée à 30 mégaoctets. Il semblait que nous avions épuisé toutes les options pour quelques optimisations automatiques et sûres simples.
Nous avons décidé d'envisager des solutions radicales. Ici, nous avons vu deux options - la création d'une application lite séparée ou le traitement de l'architecture Launcher et la transition vers une architecture modulaire avec la possibilité de construire Launcher sans modules supplémentaires. La première option est assez longue et coûteuse. Très probablement, la création d'une telle application se traduira par une application distincte à part entière pour vous, qui devra être entièrement prise en charge et développée. D'un autre côté, l'option avec une architecture modulaire est également assez chère, assez risquée, mais elle est toujours plus rapide, puisque vous travaillez déjà avec une base de code bien connue, vous avez déjà un ensemble de tests unitaires automatiques, de tests d'intégration et de tests manuels cas.
Il convient de noter que quelle que soit l'option que vous choisissez, vous devrez en quelque sorte abandonner une partie des fonctionnalités de votre application dans la version pour Android Go. C'est normal. Google fait de même dans ses applications Go.
En conséquence, après avoir implémenté une architecture modulaire, nous avons résolu de manière fiable nos problèmes de mémoire et commencé à passer des tests même sur des appareils avec un petit écran, c'est-à -dire que nous avons réduit la consommation de mémoire à 30 mégaoctets.

Un peu sur la surveillance de la mémoire, sur la façon dont nous contrôlons l'utilisation de la mémoire. Tout d'abord, nous mettons en place des analyseurs statiques, le même Lint on error dans les cas où nous utilisons enum, créons des méthodes synthétiques ou utilisons des collections non optimisées.
Plus difficile encore. Nous avons mis en place des tests d'intégration automatiques qui exécutent Launcher sur des émulateurs et décollent après un certain temps de la consommation de mémoire. S'il est très différent de la version précédente, des avertissements et des alertes sont déclenchés. Ensuite, nous commençons à étudier le problème et ne publions pas les modifications qui augmentent l'utilisation de la mémoire du lanceur.
Pour résumer. Il existe différents outils pour surveiller la mémoire, mesurer la mémoire pour un fonctionnement rapide et efficace. Il vaut mieux les utiliser tous, car ils ont leurs avantages et leurs inconvénients.
Les solutions radicales à architecture modulaire se sont avérées plus fiables et plus efficaces pour nous. Nous regrettons de ne pas les avoir pris immédiatement. Mais les étapes dont j'ai parlé au tout début du rapport n'ont pas été vaines. Nous avons remarqué que la version principale de l'application commençait à utiliser de manière optimale la mémoire, pour travailler plus rapidement. Je vous remercie