Même si cela peut paraitre un peu contradictoire dans un contexte de langage objet comme Java, minimiser le nombre d’allocations dans son programme impacte grandement sur les performances d’une application Android. Il me semblait essentiel de rédiger un article présentant cette nécessité et exposant quelques régles élémentaires à suivre.

Lorsqu’on me demande mon avis sur la VM Android j’aime utiliser l’expression : “la VM Android est tout ce qu’il y a de plus élémentaire !”. En effet, à force de jouer avec cette dernière on se rend compte qu’elle n’est pas ultra performante : c’est une simple VM interprétée. Le garbage collector (GC) n’est pas plus développé puisque c’est un basique GC “mark and sweep” (pas de GC générationnel par exemple). Android n’inclut pas non plus de technologies telles que la compilation JIT (Just In Time) ou les optimisations à la compilation (caching de variables constantes de boucle, etc.) … Pour résumer, la VM Android ressemble un peu aux premières VMs Java qui aient existé sur cette Terre ! Elle dispose néanmoins d’un énorme avantage : elle fonctionne :).

Android ne repose donc pas sur une base “ultra” performante. Ainsi le GC prend, en règle général environ 100ms à s’exécuter bloquant totalement l’exécution du programme. Cela peut paraitre dérisoire mais bloquer le thread graphique pendant 100ms provoque généralement une forme de mécontentement chez certains tous les utilisateurs ^^. Imaginez une image se déplaçant uniformément du point supérieur gauche de l’écran au point inférieur droit en 500ms. Sur un écran de 320×480 pixels, l’image parcourt environ (comme quoi Pythagore est toujours utile) sqrt(320^2 + 480^2) ≈ 577 pixels en 500ms soit environ 115 pixels toutes les 100ms. Dans le cas où le GC s’exécute durant l’animation, un blocage se fait ressentir … votre image va tout simplement “sauter” 115 pixels faisant croire à une cassure de l’animation.

Lorsque vous développez votre interface graphique, vous devez faire en sorte que cette dernière soit la plus fluide possible. Rendre une interface graphique fluide plusieurs technique dont la principale consiste à empêcher le GC de s’exécuter.Malheureusement pour les développeurs d’UI, le GC est une composante principale du Java. Il n’est pas possible de le supprimer. L’astuce consiste donc à le contourner en n’allouant/désallouant aucune ressource lorsqu’une animation (scrolling de liste, de menu, animation basique, etc.) est en cours. L’intérêt de cet article est de montrer certaines techniques permettant de satisfaire la précédente astuce.

Créer les objets au plus tôt

Il m’arrive régulièrement de lire du code ressemblant au suivant :

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
 
    // ...
 
    Paint paint = new Paint();
    paint.setColor(Color.BLACK);
    canvas.drawText(mText, 20, 20, paint);
 
    //...
}

Créer l’objet de type Paint dans la méthode onDraw(Canvas) (méthode considérée comme “critique” puisqu’elle est appelée très souvent) ralentit extrêmement l’exécution du programme et provoque des GCs lorsque trop d’objets de type Paint ont été créés. Préparer les objets (Paint, Rect, Runnable, etc.) dès la création de l’instance est bien souvent la seule chose à faire pour résoudre ce problème :

public class MyView extends View {
 
    private final Paint mPaint = new Paint();
 
    public MyView(Context context) {
        mPaint.setColor(Color.BLACK);
        // ...
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
 
       // ...
       canvas.drawText(mText, 20, 20, paint);
       //...
    }
 
}

Maitrisez l’auto-boxing/unboxing

Java 1.5 inclut une fonctionnalité qui facilite grandement le développement : l’auto-boxing/unboxing des types primitifs. Prenons l’exemple ci dessous :

private HashMap<Integer, String> mHashMap;
public MyObject() {
    mHashMap = new HashMap<Integer, String>();
    mHashMap.put(1, "My String 1");
    mHashMap.put(40, "My String 2");
    mHashMap.put(567, "My String 3");
}

Ajouter de nouveaux couples (clé,valeur) à notre HashMap se fait de façon déconcertante puisque Java utilise le type primitif int comme clé. Comment cela est-il possible puisque les clés ne peuvent normalement être que des objets (dans notre cas de type Integer). En réalité, Java 1.5 créé un objet de type Integer ne contenant qu’un int. C’est ce qu’on appelle l’auto-boxing. Malheureusement, cela signifie que Java créé des objets “inutilement”.

Pour contrer ce problème essayer de créer vos propres structures de données à base de tableaux élémentaires (oui oui je parle des tableaux avec les [] et non pas de LinkedList, Vector ou autre ArrayList). Dans l’exemple ci-dessus, il est également possible d’utiliser un objet extrêmement pratique disponible dans android.util : SparseArray. Il permet de “mapper” des objets à des int (servant de clés) même s’il existe des gaps entre les clés.

Attention aux Strings

Le type String est un type un peu spécial en Java. En effet, ce n’est pas réellement un type primitif mais le langage l’intègre tellement bien qu’il s’utilise comme tel. En réalité, les objets de type String sont “immutables”. Cela signifie qu’ils ne peuvent tout simplement pas être modifiés. L’exemple suivant implique donc une création de nouvelles Strings à chaque tour de boucle :

private String mStrings[];
 
@Override
public String toString() {
    final int count = mStrings.length;
    String res = "";
    for(int i = 0; i < count; i++) {
        res += mStrings[i];
    }
    return res;
}

L’utilisation d’objets “mutables” tels que StringBuilder accélère grandement l’exécution du programme en minimisant la création d’objets :

private String mStrings[];
 
@Override
public String toString() {
    final int count = mStrings.length;
    StringBuilder builder = new StringBuilder();
    for(int i = 0; i < count; i++) {
        builder.append(mStrings[i]);
    }
    return builder.toString();
}

Réutilisez les objets inutiles

La dernière règle élémentaire qui, de mon point de vue, est LA règle à ne pas oublier sur terminaux mobiles se résume en un mot : Réutilisez ! Lisez bien la documentation Android car certaines méthodes comme View getView (int, View, ViewGroup) de la classe Adapter fournissent des objets à réutiliser. Dans le cas d’une liste par exemple, dans laquelle chaque cellule est une View, il est beaucoup plus rapide de modifier le contenu de la vue que de créer une nouvelle View “from scratch” (ce qui implique généralement de faire un “inflate” d’une ressource XML - démarche longue et douloureuse pour le terminal).

Je pense avoir donné ici les principaux points qui me sont venus à l’esprit au moment de la rédaction de cet article. J’aurais aimé en écrire plus sur ce domaine mais je crois plus raisonnable de laisser une seconde partie pour de futurs articles. Je ne peux maintenant que vous souhaitez “bon codage” ! Codez bien, codez avec votre tête ^^.

Trackback

13 commentaires jusqu'à maintenant

  1. Merci des conseils !
    Comment faire pour empêcher le GC de s’exécuter ?

  2. Euh tu as bien lu mon article? J’explique bien que c’est pas possible mais qu’on peut faire en sorte qu’il “passe ” le moins possible.

  3. Très bon article, comme lien annexe je mettrai la vidéo de Romain Guy : “Make your Android UI Fast and Efficient” (http://www.youtube.com/watch?v=N6YdwzAvwOA)

    Des que je met la main sur un sample du SMiT MiD-560 ej teste tout ça ^^

  4. Cool cet article.
    Question par rapport au dernier point.
    Dans getView, si on réutilise le view passé en paramêtre, comment spécifier le layout xml ?
    Je pense que justement en créant une nouvelle view et en utilisant inflate, c’est moins couteux que d’allouer de nouveaux objet LinearLayout ou autre (et c’est plus de ligne de code également).

  5. Super article cyril…
    Si tu as des solutions pour optimiser les appels aux DB je suis également preneur ou plutôt je pourrais t’indiquer les méthodes couteuses !

  6. @Dami : Oui tu as raison, il y a des points similaires expliqués en anglais.

    @benjamin : Dans getView, si tu réutilise le paramètre “convertView”, il n’y a pas besoin de spécifier le layout puisque tu a déjà une vue (une vue déjà initialisée). Je pense que ce que tu recherche c’est “comment accéder à une sous vue de cette cellule”. Il te suffit d’utiliser son id.
    Je ne comprend pas ta dernière phrase : Créer une vue c’est allouer un objet. N’hésite pas à m’expliquer plus précisément ton problème pour que je t’explique. :)

  7. Exemple à ne pas faire : http://r.android.com/#patch,sidebyside,12661,1,src/com/android/calendar/CalendarView.java

    Suivez les recommendations de cet article :)

  8. @Romain Guy : Effectivement :)

  9. [...] Eviter l’allocation d’objets [...]

  10. Jérémy @ 2010-01-14 17:05

    Un autre truc méconnu concernant l’usage de ma méthode substring sur un objet de type String.
    Le String renvoyé par la méthode substring est particulier car il n’est en réalité pas constitué de la sous chaine, mais uniquement du pointeur dans le tableau de char et d’une référence sur le tableau de char constituant la chaine d’origine.
    Donc en gros, si vous utilisez substring pour éclater une grosse chaine en chaine plus petite, il n’y a en réalité aucun gain à attendre, la grosse chaine restera en mémoire jusqu’à ce que toutes les sous chaines aient été libérées par le GC.

  11. Jérémy @ 2010-01-14 17:07

    Erratum :
    il faut lire “la” méthode substring et non pas “ma” méthode substring, ce n’est pas moi qui l’ai codée :-)

  12. Petite question, si jamais vous avez testé : en général le compilateur java classique interprète les concaténations “monolignes” de String comme l’utilisation d’un StringBuffer. ex :

    String s = “salut ” + name + ” !”;

    est compilé en tant que :

    String s = new StringBuilder(”salut “).append(name).append(” !”).toString();

    (ce qui évite au dév d’allourdir visuellement son code avec des StringBuffer juste pour ça)

    Avez-vous pu constater la même chose sous Android ?

  13. @Thomas : En effet, tu as raison. Le processus de compilation d’Android utilise le JDK installé sur la machine de développement et il n’y a donc aucune différence entre le code généré pour serveur ou PC … la différence s’effectue après grâce au compilateur Dalvik. Les StringBuffer/Builder sont donc bien “générés” mais de façon bête et stupide. Imaginons, par exemple, une boucle for :

    final String cyril = “Cyril”;
    String s;
    for (int i = 0; i < 10; i++) {
    s += “salut” + cyril + “!”;
    }

    Ce code va bien générer du code à base de StringBuilder/Buffer. L’inconvénient c’est qu’il y aura 10 StringBuffer/Builder créés (et autant de String intermédiaires) et non pas un seul et unique. Il est donc préférable de faire :

    final String cyril = “Cyril”;
    final StringBuilder builder = new StringBuilder();
    for (int i = 0; i < 10; i++) {
    builder.append(”salut”).append(cyril).append(”!”);
    }
    String s = builder.toString();

Ajoutez votre commentaire maintenant