Cyril Mottier

“It’s the little details that are vital. Little things make big things happen.” – John Wooden

Accélérez Votre UI en Minimisant l'Allocation d'Objets

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 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 320x480 pixels, l'image parcourt environ (comme quoi Pythagore est toujours utile) sqrt(3202 + 4802) ≈ 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 :

1
2
3
4
5
6
7
8
9
10
11
12
@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 :

MyView.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 :

1
2
3
4
5
6
7
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 :

1
2
3
4
5
6
7
8
9
10
11
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 :

1
2
3
4
5
6
7
8
9
10
11
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.