Accélérer l’UI en minimisant l'allocation d'objets

Cyril Mottier

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.

N'hésitez pas à nous faire part de votre avis sur cet article : Commentez ♪

- Titre : Accélérer l’UI en minimisant l'allocation d'objets

- Auteur : Cyril Mottier

- Parution : 9 décembre 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 09/12/2010 Cyril Mottier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Préambule

- Créer les objets au plus tôt

- Maîtrisez l'autoboxing/unboxing

- Attention aux Strings

- Réutilisez les objets inutiles

- Remerciements

Préambule

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 VM 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érale environ 100 ms à s'exécuter bloquant totalement l'exécution du programme. Cela peut paraitre dérisoire, mais bloquer le thread graphique pendant 100 ms provoque généralement une forme de mécontentement chez certains, voire 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 500 ms. 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 500 ms soit environ 115 pixels toutes les 100 ms. 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 techniques 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 GC 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, mPaint);
//...
}
Â
}

Maîtrisez l'autoboxing/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ée 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ée des objets « inutilement ».

Pour contrer ce problème, essayez 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.

Remerciements

Un grand merci à jpcheck pour sa relecture.