
Optimisation de la mémoire pour les applications de traitement de l’image sous Android.
Perdues dans la masse des applications Android disponibles sur le AppMarket de Google (250 000 à ce jour), nos applications mobiles doivent présenter un contenu média riche et attrayant afin de se démarquer de la compétition. De plus, de nombreuses fonctionnalités à la mode nécessitent des manipulations d’image élaborées: traitement numérique de l’image, réalité augmentée, reconnaissance de texte ou de visage. Cet article s’adresse aux développeurs Android débutants. Il présente plusieurs techniques d’optimisation de la mémoire pour les applications de traitement de l’image sous Android. Il est basé sur les cas d’optimisations les plus fréquents auxquels nous avons eu à faire face au cours du développement de plusieurs de nos applications.
Pas de problème: le garbage collector s’en occupe!

Sous Android, les allocations d’image ne sont pas gérées comme le reste de la mémoire virtuelle sous Java. Les objets de type Bitmap sont placés directement en mémoire native et celle-ci est limité à un maigre 16MB sur la plupart des appareils, (24MB sur des appareils plus performants tels le Droid ou le Nexus One) De plus, la taille de l’image en mémoire est indépendante de la taille du fichier jpg compressé dont elle provient. Par exemple, une image de 2 megapixels occupera 8MB en mémoire (4 octets par pixel), et ce même si le fichier compressé jpg n’occupe que quelques centaines de KB sur disque. C’est pourquoi une gestion minutieuse de la mémoire est requise afin d’éviter l’interruption abrupte de l’activité en cours et l’apparition du le fameux message « OutOfMemory Error » (plus de mémoire disponible), aussi inélégant pour l’utilisateur qu’humiliant pour le développeur!
Dans le monde Java, le garbage collector est responsable de libérer la mémoire inutilisée par le programme. Les images n’échappent pas à cette règle: lorsqu’une image n’est plus référencée, elle est libérée automatiquement par le système. Malheureusement, la libération des objets inutilisés n’est pas immédiate car le garbage collector s’exécute en arrière plan, et un délai existe entre le moment ou un objet n’est plus référencé et la récupération de l’espace mémoire utilisé.
Soyez dans le vent: recyclez vos Bitmaps!
Le comportement en arrière-plan du garbage collector peut causer problème lorsque, par exemple, on doit traiter une série d’images en boucle: on risque alors d’allouer de la mémoire supplémentaire alors que le garbage collector n’a pas eu le temps de libérer la mémoire non référencée par l’itération précédente. C’est pourquoi Android offre la possibilité de libérer la mémoire graphique instantanément à l’aide de la fonction Bitmap.recycle(); Un appel à cette fonction détruira l’image sur le champ, dans le filet d’exécution en cours:
// Boucle de traitement d’imagefor( int i = 0 ; i < count; i++ ){ // ouvrir l’image
Bitmap bitmap = BitmapFactory.decodeFile( »chemin d’accès », options ); // traiter l’image
… // recycler l’image
bitmap.recycle(); }
Important: après avoir été recyclé, un bitmap n’est plus disponible pour l’affichage ou tout autre accès aux pixels qui le composent.
Diète recommandée pour application vorace: BitmapFactory.Options
Nous avons fréquemment à afficher une liste dont les items contiennent des images timbres (représentation miniature d’images plus grandes, ou thumbnails). Par exemple: les items d’une liste de clavardage présentera une photo du participant accompagnée du texte de son intervention ; un album photo présentera une liste de photos accompagnées d’un titre et de la date, etc. Or que faire si, comme dans ce dernier cas, les images sources sont de grande taille ? Existe-t-il un moyen de présenter une miniature sans charger en mémoire les 8Mb de l’image source?
Le SDK de Android offre la possibilité d’obtenir des images timbres sans avoir à surcharger la mémoire: le principe consiste à ne pas lire tous les pixels de l’image mais seulement une fraction des pixels requis, grâce à l’objet BitmapFactory.Options.inSample. Une valeur de 2 produira une image 4 fois plus petite, une valeurs de 3, une image 9 fois plus petite, et ainsi de suite. Voici un exemple d’utilisation de cette technique:
// Créer un objet de type BitmapOptionsBitmapFactory.Options options = new BitmapFactory.Options(); // Cette option indique qu’on ne veut obtenir que la taille de l’image,
// sans charger les pixels en mémoire
options.inJustDecodeBounds = true; // Ouvrir le fichier d’image pour obtenir sa taille
BitmapFactory.decodeFile( »chemin d’accès », options ); // Les options contiennent maintenant la taille de l’image
int width = options.outWidth;
int height = options.outHeight; // Référence au bitmap ouvert
Bitmap bitmap = null; // Si l’image est trop grande: ouvrir en mode inSample
if( isImageTooLarge( width, height ) ){ // Calculer la taille d’échantillon
// Une valeur de 2 produira une image 4 fois plus petite en mémoire
int sampleSize = calculateSampleSize( width , height ); // Créer l’option inSample
BitmapFactory.Options sampleOptions = new BitmapFactory.Options();
sampleOptions.inSampleSize = sampleSize; // Ouvrir l’image en mode inSample
bitmap = BitmapFactory.decodeFile( »chemin d’accès », sampleOptions); }
else { // Ouvrir l’image en mode normal
bitmap = BitmapFactory.decodeFile( »chemin d’accès » ); }
Le choix de la valeur de taille d’échantillon (sampleSize) dépend de plusieurs critères. Une valeur de l’ensemble {2 exposant n} offrira plus de rapidité comparée à un nombre impair; une valeur trop grande offrira une image visiblement dégradée. Cet inconvénient peut-être compensé en utilisant une valeur moindre pour inSampleSize et procéder à un redimensionnement ultérieur.
Une autre option intéressante: inPrefferedConfig
Par défaut, les images sous Android sont traitées dans l’espace-couleur ARGB_8888. Dans cette espace, chaque pixel est représenté par 4 octets, un pour chaque canal (Rouge – Vert – Bleu – Alpha). Le canal alpha représente la transparence du pixel. Si l’image source ne contient pas de canal alpha, l’octet Alpha sera ignoré par le décodeur mais tout de même alloué en mémoire, puisque les microcontrôleurs modernes alignent les octets en nombre pair. Par conséquent, aucun gain de mémoire ne sera observé si l’image source ne contient pas de canal alpha.
Il est toutefois possible de diviser par deux l’espace mémoire d’une image en utilisant l’espace-couleur RGB_565. Dans cet espace, chaque pixel est placé dans deux octet utilisant 5 bit pour les canaux rouge et bleu, et 6 bits pour le canal vert. (Noter qu’une plus grande définition est réservée au canal vert, ce qui témoigne du fait que le vert est la couleur dont l’œil humain reconnait le plus de nuances) Évidement, l’image subira une dégradation qui n’échappera pas à l’œil d’un expert mais l’utilisateur moyen percevra rarement la différence. Cette option est donc tout à fait valable pour les applications de présentation d’image habituelles. Voici un exemple de code qui permet d’ouvrir une image en format RGB565.
// Créer un objet de type BitmapOptionsBitmapFactory.Options options = new BitmapFactory.Options(); // Ouvrir l’image en mode RGB_565
options.inPreferredConfig = Bitmap.Config.RGB_565; // Ouvrir le fichier d’image
Bitmap bitmap = BitmapFactory.decodeFile( »chemin d’accès », options );
Note: il existe un espace-couleur encore plus compact: ALPHA_8 qui représente chaque pixel par un seul octet représentant la transparence du pixel. Cet espace est utilisé pour les applications de reconnaissance et d’analyse des caractéristiques d’une image.
Un ami encombrant: AsyncTask
Le traitement d’image est une tâche qui requiert parfois beaucoup de temps de calcul et une bonne habitude consiste à exécuter ce type de tâche en arrière plan afin d’éviter de bloquer le filet d’exécution de l’interface usager. En Java, on utilise les objets de type Thread pour procéder au traitement multi-filet et l’objet Handler pour échanger l’information entre plusieurs filets. Android offre la classe AsyncTask afin de simplifier la synchronisation entre le filet d’interface usager et les filets d’arrière plan. Cet objet doit cependant être utilisé avec soin car il peut générer d’importantes fuites de mémoire.
En effet, dans la plupart des exemples d’utilisation de l’objet AsyncTask offerts par google sur le site developer.android.com, un objet hérité de l’AsyncTask est embarqué (private embedded class) à l’intérieur d’un objet de type Activity, responsable du contrôle de l’interface usager. Ce type d’embarquement n’est acceptable que pour les applications ne comprenant qu’une seule activité pour la raison suivante:
Lors de leur lancement, les objets de type AsyncTask sont référencés par le système d’exploitation pour une durée indéterminé, généralement plus longue que la durée de vie de l’activité appellante, et le développeur n’a aucun contrôle sur la déallocation de la tâche ainsi conservée. Or, dans le cas des classes embarquées, une référence cachée existe dans le système d’exploitation entre la classe privée interne et la classe externe. Cette référence cachée sur l’activité appelante empêchera le garbage collector de désallouer la classe externe tant que la classe interne ne sera pas déréférencée. Or, puisque l’AsyncTask continue d’être référencée par le système d’exploitation indépendamment de l’activité appelante, il en résulte une fuite mémoire de l’activité au grand complet, même ci celle-ci n’est plus utilisée par l’application!
C’est pourquoi deux pratiques sont nécessaire (parfois vitales) pour éviter cette lourde fuite mémoire:
- Les classes embarquées surchargeant l’objet AsyncTask doivent être déclarées statiques, ou dans un fichier externe, afin d’éviter la référence cachées d’embarquement.
- Les objets de type AsyncTask ne doivent contenir aucune référence forte vers l’activité appelante. Si le filet d’arrière plan doit communiquer avec l’activité appelante, ce doit être au moyen d’une référence de type WeakReference(). De cette façon, l’activité appelante sera désallouée même si la tâche d’arrière plan continu d’exister dans le système d’exploitation.
exemple:
public class MyPublicTask extends AsyncTask<Void, Void, Void> { WeakReference< Context > context; public MyPublicTask( Context context ) { super();this.context = new WeakReference< Context >( context ); } // Implementation de l’AsyncTask // … }
Un avenir radieux
Voila, vous êtes maintenant prêts à vous lancer dans le développement d’une application Android d’imagerie numérique sans frapper les écueils les plus fréquemment rencontrés. Voyons maintenant ce à quoi on peut s’attendre au cours des prochaines années.
À court terme, on peut anticiper que la mémoire disponible sur les appareils de téléphonie mobile s’étende au delà des limites actuelles. Toutefois, la taille des images générées par les caméras embarquées risque également de s’accroitre d’autant, le problème demeurera donc entier. Par ailleurs, l’équipe de développement Android chez Google promet des améliorations significatives aux performances du garbage collector actuel.
Dans un avenir plus éloigné, il est probable que les navigateurs mobiles seront dotés d’extensions graphiques évoluées comme webGL – disponible dès maintenant sous Chrome et Safari dans leur version desktop – permettant le texturage et la géométrie openGL avancés en language Javascript. Jetez un coup d’oeil sur cette page de demonstration. pour un avant goût impressionnant. L’iOS-5 supporte déjà cette extension sous certaines réserves. Avouez que cela fait rêver!

Groupe Informatique TechSolCom est une firme TI de services conseils et de développement logiciel présente depuis 2003 et dont l’équipe est déjà composée de plus de 180 conseillers. L’entreprise est structurée via des centres d’expertises et d’une équipe de développement interne pour les plate-formes iOS(Apple) et Android.
L’équipe TSC Mobile (www.techsolcom.ca)








Laisser un commentaire