Accueil > Mobile, Outillage > Android : optimisation des performances 2 – La mémoire

Android : optimisation des performances 2 – La mémoire

Dans l’article précédent, j’avais abordé les problèmes de fluidité et d’asynchronisme. Aujourd’hui, je vais aborder un autre sujet très important quand il s’agit d’optimiser des applications Android : la mémoire.

La mémoire disponible pour une application Android est très limitée par rapport à une application “desktop”. Cette limite dépend des appareils, mais dans le pire des cas celle-ci peut être de 16 Mo. Il faut donc faire très attention à ne jamais s’en approcher, car cela peut-être une source d’instabilité : si l’application ne dispose plus de mémoire, elle risque de crasher en générant une erreur de type “OutOfMemoryError”.

Dans le cadre d’un algorithme qui aurait besoin d’analyser de grosses quantités de données, il faut éviter de garder en mémoire la totalité des données, et essayer de traiter les donnés au fil de l’eau. Par exemple, si on a besoin de parser un gros document XML, il faut préférer l’utilisation d’une API de type SAX, qui traite les données au fur et à mesure qu’elles arrivent, plutôt que de charger l’intégralité du document en mémoire et l’analyser ensuite (ex : API DOM). Plus généralement, il ne faut pas hésiter à abuser des flux (InputStream/OutputStream) qui permettent la lecture/écriture de données en sollicitant très peu de mémoire.

Exemple simple : vous voulez télécharger un fichier depuis le web, et le stocker sur la carte SD. Une mauvaise approche consisterait à télécharger entièrement les données pour les stocker dans un tableau byte[], et une fois le téléchargement terminé, écrire les données de ce tableau dans le fichier :

private void download(InputStream inputStream, FileOutputStream outputStream) {
    byte[] buffer = readAllData(inputStream);
    writeAllData(outputStream, buffer);
}

Si le fichier à télécharger fait 100 ko, ça marche, s’il fait 100 Mo, l’application plante. Une meilleure solution consiste à lire et écrire les données au fur et à mesure, par petits blocs :

private void download(InputStream inputStream, FileOutputStream outputStream) {
    byte[] buffer = new byte[BLOCK_SIZE];
    int bytesRead;
    while ((bytesRead = inputStream.read(buffer)) != 0) {
        outputStream.write(buffer, 0, bytesRead);
    }
}

Les images sont aussi très gourmandes en mémoire. Par exemple, si votre application a besoin d’afficher un grand nombre d’images téléchargés, vous allez potentiellement vouloir les garder en mémoire pour pouvoir les réafficher plus rapidement : plutôt que de garder en mémoire une grosse quantité d’objets Bitmap, stockez les images sur le système de fichier. Mieux, une solution intermédiaire consiste à mixer les deux, en créant un cache mémoire à taille limitée. Voir notamment cet article sur la création de caches d’images.

La quantité de mémoire utilisée par une application peut être obtenue depuis la vue « Heap » de la perspective DDMS. Après avoir sélectionné l’application dans la vue « Devices », il faut cliquer sur le bouton « Update Heap » de cette même vue. La vue « Heap » affiche alors la quantité de mémoire utilisée par l’application (cliquez sur « Cause GC » si les données n’apparaissent pas tout de suite). Dans cette vue, la valeur qu’il faut surveiller est « Allocated », c’est-à-dire la quantité de mémoire allouée actuellement. La colonne « % Used » est à prendre avec des pincettes, car c’est le pourcentage de mémoire utilisée par rapport à taille du tas (heap), mais cette dernière varie en fonction des besoins (jusqu’au maximum autorisé).

Dans le cas où l’application a tendance à crasher pour des problèmes de mémoire, cet outil peut servir à savoir à quel moment l’utilisation de la mémoire devient importante, si cela arrive d’un coup ou progressivement, etc. Pour aller plus loin, il va falloir faire appel à Memory Analyzer.

Memory Analyzer

Il peut arriver que l’application pose un problème d’utilisation mémoire sans qu’on sache précisément d’où vient le problème. Dans ce cas, il existe une fonctionnalité du SDK qui permet de dumper la mémoire au format HPROF afin de pouvoir ensuite l’analyser avec le plugin Eclipse Memory Analyzer (il faut l’installer au préalable).

Pour cela, il faut lancer l’application (sur un terminal ou un émulateur), puis depuis la fenêtre “Devices”, sélectionner le processus correspondant l’application, puis cliquer sur le bouton “Dump HPROF File”.


L’opération peut prendre quelques secondes. Dès que l’emprunte mémoire est récupérée, le plugin Memory Analyzer ouvre automatiquement le fichier HPROF, et l’analyse peut commencer. Ce plugin offre plusieurs vues permettant de faire une analyse mémoire sous plusieurs angles, mais la plus intéressante selon moi est la vue “Dominator Tree”, qui affiche la liste des objets qui prennent le plus de mémoire.


Dans la première colonne sont affichés les objets, nommés par leur classe et hashcode.

La colonne suivante, “Shallow Heap” affiche la quantité de mémoire utilisée par chaque objet, en octets.

La colonne “Retained Heap” équivaut à la quantité de mémoire utilisée par chaque objet (Shallow Heap), à laquelle on rajoute la mémoire utilisée par les objets dépendants. C’est l’indicateur le plus intéressant, car il correspond à la quantité de mémoire qui pourrait être libérée si l’objet était libéré. Voir explication détaillée.

La colonne “Percentage” représente le rapport entre mémoire retenue par un objet (retained heap) et mémoire totale.

Exemple : sur la capture d’écran ci-dessus, on voit sur la 3ème ligne qu’il existe un objet Bitmap (une image) pour lequel est alloué 1 Mo de mémoire. Il s’agit sûrement d’une image de 512×512 pixels (car 512*512*4 = 1M). On peut alors se poser la question de l’utilité de cette image, et poursuivre l’investigation en inspectant cet objet, ses références entrantes et sortantes, etc.

Cette vue permet donc de repérer les objets les plus importants, mais si le problème vient de l’utilisation d’un trop grand nombre de petits objets, cette vue est inefficace. Il faut alors utiliser la vue “Histogram”, qui affiche, en fonction du type, le nombre d’objets instanciés et la mémoire utilisée.


Pour chaque type, il est ensuite possible d’afficher la liste complète des objets et d’inspecter leur contenu. Il est aussi possible d’afficher des graphes de dépendances (le même qu’utilise le GC pour déterminer les objets à libérer), ce qui peut permettre de remonter à la source du problème.

Dans la capture d’écran ci-dessus, on peut voir sur la première ligne que 7,5 Mo de mémoire sont occupés par des byte[] (tableaux d’octets bruts). Cela n’a rien d’étonnant quand on sait que les images sont stockées dans des byte[] et que c’est souvent ce qui prend le plus de mémoire dans une application. D’ailleurs, on peut voir sur la dernière ligne qu’il existe 282 objets Bitmaps, et que ceux-ci retiennent plus de 7 Mo de mémoire, ce qui confirme ce raisonnement : Bitmap est une classe Android utilisée pour encapsuler des images, et les données brutes de l’image sont stockées à l’intérieur d’un objet Bitmap dans un byte[].

En parlant de Bitmaps, il y a une chose importante à savoir : avant Honeycomb (3.0), les données des images étaient allouées sur une mémoire externe à la VM Dalvik, il était donc impossible d’analyser ces données avec Memory Analyzer ou la vue « Heap ». Le travail est donc plus compliqué quand on veut résoudre des problèmes de mémoires sur un appareil <3.0 car on ne voit que la partie émergée de l’Iceberg…

A lire aussi : http://android-developers.blogspot.fr/2011/03/memory-analysis-for-android.html

La semaine prochaine, nous verrons les alternatives existantes pour programmer en code natif afin d’écrire des programmes ultra-performants avec le NDK et RenderScript.

Categories: Mobile, Outillage Tags: , ,
  1. Pas encore de commentaire
  1. Pas encore de trackbacks


3 − deux =