Accueil > Mobile, Outillage > Android : optimisation des performances 1 – Fluidité de l’interface

Android : optimisation des performances 1 – Fluidité de l’interface

Le but de cette série d’articles est de vous aider à réaliser des applications Android performantes, en vous présentant les bonnes pratiques, les pièges à éviter, et surtout les outils que vous avez à votre disposition pour vous aider dans les différentes étapes de l’optimisation. Cet article s’adresse aux développeurs d’applications Android, quelques bases sont donc requises.

Les problèmes de performances peuvent être de nature variée :

  • Fluidité de l’interface
  • Rapidité des traitements
  • Instabilité (pour cause de manque de mémoire)
  • Utilisation trop intensive de la batterie (dans le cas de traitements en arrière-plan)

Pour les habitués des applications “desktop” ou “web”, il faut être conscient que l’application va tourner sur des appareils beaucoup plus limités qu’un ordinateur classique. Les CPUs sont généralement moins puissants, et la quantité de mémoire disponible est beaucoup plus faible. Surtout, les appareils existants sont très variés en terme de capacités, et si une application veut cibler le plus d’appareils possibles, elle se doit de tourner correctement sur les smartphones sortis il y a quelques années. Si vous avez un HTC Magic sous la main, c’est l’idéal, car il compte parmi les plus vieux et les moins puissants.

Il faut toutefois être prudent car la mise en œuvre d’optimisations peut avoir un impact sur la maintenabilité du code en réduisant sa lisibilité et en augmentant la complexité. Elles doivent donc être utilisées avec pragmatisme, et certaines ne devraient pas être appliquées si le gain de performance est trop minime. En outre, il faut toujours mesurer avant de faire une optimisation, afin d’être sûr qu’il y a un réel besoin, de cibler le problème, et de pouvoir comparer les mesures avec celles que l’on effectuera une fois l’optimisation réalisée (parfois pour se rendre compte que ça n’a servi à rien…).

Nous allons ici nous concentrer sur les problèmes de fluidité, en prenant le cas des ListViews. Si une ListView saccade lors d’un déroulement vers le bas ou vers le haut, c’est qu’elle est probablement mal construite : la création ou le rendu des éléments de la liste prend trop de temps. Deux facteurs peuvent en être la cause :

  • une hiérarchie de vues trop complexe
  • un blocage du thread principal à cause d’opérations d’entrée/sortie bloquantes

Cas où la hiérarchie des vues est trop complexe

La construction des vues est un processus récursif. Plus l’arborescence des vues est profonde, plus la phase de construction va être longue. Dans ce cas, la seule solution consiste à simplifier la hiérarchie de vues. En premier, il faut essayer de retirer quelques éléments qui ne sont pas vitaux. Ensuite, il faut préférer l’usage de RelativeLayouts, plutôt que faire des enchaînements complexes de layouts. Ce type de layout permet de créer des vues complexes tout en gardant une hiérarchie simple.

En résumé :

  • ne pas charger les écrans
  • utiliser des RelativeLayout quand c’est possible

Le SDK fournit un outil qui permet d’analyser les layouts et leurs performances : hierarchyviewer. Cet outil permet de visualiser l’arbre des vues, les propriétés de chaque vue, et affiche des indicateurs de performance qui permettent d’identifier les vues qui prennent le plus de temps à être créées.

Les points colorés en vert, jaune, ou rouge sont des indicateurs de performance. Ces indicateurs représentent la vitesse de rendu d’une vue, par rapport aux autres vues. Il y a trois indicateurs, un pour chaque étape du rendu : measure, layout, draw.
Voir aussi : http://developer.android.com/tools/debugging/debugging-ui.html

Cas où le thread principal fait des traitements bloquants

La vue est construite sur le thread principal, appelé “main Thread”, ou “Thread UI”. Par défaut, le code qu’on écrit est exécuté dans ce thread. Si l’application a besoin de faire des traitements longs, comme des accès disques, ou des requêtes HTTP, ces traitements doivent être faits sur d’autres Thread, en asynchrone. Dans le cas contraire, l’interface risque d’être bloquée, et si le blocage est trop long, on risque une ANR.

Dans le cadre de notre ListView, il se peut qu’on veuille afficher, à l’intérieur d’un élément de la liste, une image dont on possède l’URL. Si cette image est téléchargée et générée directement depuis le getView() de l’Adapter, l’interface va être bloquée pendant plusieurs secondes, ce qui n’est pas envisageable. Il faut donc faire ce traitement en asynchrone.

D’une manière générale, il faut éviter tous les traitements longs sur le thread principal, tels que les accès disque, base de données, réseau, et les traitements CPU complexes comme le parsing, le traitement d’images, etc.

Faire des traitements asynchrones

Pour faire des traitements asynchrones, il existe plusieurs méthodes : créer un Thread fils, utiliser les AsyncTasks, passer directement par un ThreadPoolExecutor… mais toutes ces méthodes reviennent au même : dans tous les cas, il s’agit d’exécuter du code sur un autre Thread, et une fois le traitement terminé, revenir sur le thread principal pour mettre à jour la vue. (Je précise que les vues ne doivent pas être modifiées depuis un thread autre que le thread principal, sous peine de dysfonctionnements graves.)

Exemple simple : on a besoin d’afficher une image dont on connaît l’URL. On va d’abord initialiser une ImageView, puis on va télécharger l’image dans un autre Thread, et enfin on va revenir sur le thread principal pour appliquer l’image à l’ImageView.

final ImageView imageView = (ImageView) view.findViewById(R.id.image);
imageView.setImageBitmap(null); // supprime l'image précédente

new Thread() {
    public void run() {
        final Bitmap bitmap = downloadBitmap(url);

        // ce Runnable sera exécuté sur le thread principal
        runOnUiThread(new Runnable() {
            public void run() {
                imageView.setImageBitmap(bitmap);
            }
        });
    }
}.start();

Cette méthode est très simple mais peu lisible car elle nécessite l’imbrication de classes anonymes. Toutefois le concept est là, à vous de le mettre en pratique selon vos besoins. On peut aussi utiliser AsyncTask, une classe qui a pour but de simplifier la gestion de l’asynchronisme en abstrayant de cette gestion des threads. De plus, les AsyncTask sont plus performantes car les threads sont réutilisés. Pour le même exemple avec une AsyncTask, cela ressemblerait à :

final ImageView imageView = (ImageView) view.findViewById(R.id.image);
imageView.setImageBitmap(null);

new AsyncTack<String, Void, Bitmap>() {

    protected Bitmap doInBackground(String... params) {
        String url = params[0];
        Bitmap bitmap = downloadBitmap(url);
        return bitmap;
    }

    protected void onPostExecute(Bitmap bitmap) {
        imageView.setImageBitmap(bitmap);
    }
}.execute(url);

On pourra ensuite en faire une classe réutilisable si on a besoin de faire la même chose à plusieurs endroits.

Traceview

Parfois il peut être difficile de détecter la source des ralentissements. Heureusement, l’outillage du SDK Android est suffisamment fourni, et pour cela l’outil traceview permet d’inspecter le déroulement de l’application sur un intervalle de temps donné. Une fois activé, cet outil va mémoriser tous les appels de fonctions faits par l’application, et va représenter ces appels sur un graphe chronologique. Il faut le lancer au bon moment, et l’arrêter dès que les algorithmes à inspecter ont été exécutés, afin d’éviter de polluer le résultat avec des informations en trop.

Pour activer la collecte d’information traceview, il faut sélectionner son application et cliquer sur bouton “Start Method Profiling” dans le vue DDMS. Il faut cliquer à nouveau sur ce bouton pour l’arrêter et récupérer le résultat.

La partie supérieure de l’écran de résultat affiche la chronologie d’exécution, les couleurs correspondant à des méthodes listés dans la partie inférieure. On peut passer le curseur dessus pour avoir plus d’information à instant donné. A gauche est affiché le thread concerné : s’il y a eu plusieurs threads sollicités, plusieurs timelines s’afficheront les unes sous les autres.

La partie inférieure de l’écran affiche, pour chaque méthode appelée durant la phase de profiling, différentes statistiques :

  • Incl Cpu Time : temps cumulé pendant lequel la méthode était en cours d’exécution, en incluant les appels à d’autres méthodes faits par cette méthode
  • Excl Cpu Time : idem, mais cette fois sans compter les appels à d’autres méthodes faits par cette méthode
  • Calls+RecurCalls : nombre de fois où cette méthode a été appelée + nombre de fois où cette méthode s’est appelée elle-même
  • Cpu Time/Call : temps moyen d’exécution de la méthode

Tout ceci permet d’identifier les méthodes qui prennent trop de temps à l’intérieur du thread principal, mais plus généralement traceview peut aussi être utile dans d’autres contextes liés à des problématiques d’algorithmes trop lents.

Nous verrons la semaine prochaine comment surveiller et optimiser notre utilisation mémoire afin d’éviter les ralentissements et crashs qui peuvent se produire lorsque la mémoire est trop sollicitée.

Categories: Mobile, Outillage Tags: , ,


3 + deux =