Cyril Mottier

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

Android Et La Programmation Concurrente - Partie 2

Lors de la précédente partie de cette série, nous avons introduit la notion de programmation concurrente sous Android. Nous avons, par exemple, découvert les notions d'ANRs. Il a été fait mention des raisons de l'apparition de ces boites de dialogues et de la nécessité de faire en sorte qu'elles ne s'affichent jamais dans vos applications. Malgré l'absence de lignes de code dans la précédente partie, il est important de bien avoir compris les notions qui y ont été introduites. Si vous n'avez pas eu l'occasion de vous plonger dans la partie 1, je vous conseille donc vivement de la lire avec attention à la page suivante :

Android et la programmation concurrente - Partie 1

Le système de message Android

Android facilite énormément la programmation inter-thread grâce à un ingénieux système de messages. Etant particulièrement attaché à cette notion j'ai souhaité la présenter comme première alternative à la programmation concurrente sous Android (pour ne rien vous cacher, c'est à mon sens la meilleure technique pour résoudre ce genre de problématiques sous Android)

Définitions

  • Thread : Souvent appelés processus légers, les threads peuvent être considéré comme des tâches dans lesquels s'exécutent des instructions. Dès lors qu'une application Android démarre, un thread est créé (le main thread) et sert de réceptacle pour les instructions du système. Pour donner une illusion de parallélisme, il est possible de créer plusieurs threads dans une application. Chacun des threads exécute de façon alternative (mais rapide d'où l'impression de parallélisme ou de simultanéité) des instructions. Le schéma ci-dessous montre une application disposant de 3 threads :

  • Looper : Le Thread est la base de la programmation concurrente … Malheureusement utilisé tel quel il n'a que peu d'utilité. En effet si on regarde attentivement le schéma précédent, on se rend compte qu'un thread s'arrête dès lors qu'il a fini d'exécuter ses instructions. Pour éviter cela, il est possible de faire en sorte qu'il boucle indéfiniment en attente d'instructions à exécuter. On met alors en place un Looper qu'on appelle parfois également “boucle évènementielle”. La classe Looper permet de préparer un Thread à la lecture répétitive d'actions. Un tel Thread, présenté dans la figure ci-dessous, est souvent appelé looper thread. Sous Android, le main thread est en réalité un looper thread. Un Looper étant propre à un unique Thread, il est implémenté sous la forme du design pattern TLS ou Thread Local Storage (les plus curieux pourront jeter un oeil à la classe ThreadLocal dans la documentation Java ou Android).

  • Message : Un Message représente une ou un ensemble de commandes à exécuter. Dans la définition précédente, le Message fait donc office d'instructions.

  • Handler : Cette classe vous permet d'interagir avec les looper threads. C'est par le biais d'un Handler qu'il vous sera possible de poster des Messages ou des Runnables dans le Looper qui seront exécutés (au plus vite, après un temps donné ou à un moment donné) par le thread looper pointé par le Handler (bravo à ceux qui ont compris cette phrase bourrée de vocabulaire propre à Android en une seule et unique lecture). Le code ci-dessous permet de bien comprendre l'utilisation du Handler. Ce code consiste à créer une Activity se terminant après un laps de temps définit.

BasicHandlerActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.cyrilmottier.android.asyncprogdemo;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;

public class BasicHandlerActivity extends Activity {

    private static final long DELAY = 5000;
    /*
     * On créé un Handler associé au Looper courant. Cette initialisation étant
     * exécutée au chargement de la classe par le main thread, ce Handler est
     * associé au main thread.
     */
    private final Handler mHandler = new Handler();

    private Runnable mFinisher = new Runnable() {
        @Override
        public void run() {
            finish();
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        /*
         * On poste ici le Runnable mFinisher pour qu'il ne s'exécute qu'après
         * un temps DELAY.
         */
        mHandler.postDelayed(mFinisher, DELAY);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        /*
         * On supprime mFinisher (si ce dernier n'a pas encore été exécuté) pour
         * éviter de fermer une activité potentiellement déjà fermée (cas de
         * l'utilisateur ayant fermé l'activité).
         */
        mHandler.removeCallbacks(mFinisher);
    }
}
  • HandlerThread : Créer un thread looper via code est une tâche qui peut s'avérer répétitive et sujette à erreur. Pour vous éviter de créer vous même des threads disposant d'un Looper, le framework Android propose la classe HandlerThread. Contrairement à ce que laisse penser le nom de cette classe, un HandlerThread n'associe aucun Handler au Looper.

Module de téléchargement d'images

Fort des définitions précédentes, nous sommes maintenant parés pour développer un petite module de téléchargement d'images sur Internet. Pour ce faire nous allons concevoir un système téléchargeant des images en tâche de fond. Vous trouverez l'intégralité du code de l'application dans le zip à l'adresse suivante:

Plutôt que de longs discours, j'ai préféré commenter le code de la classe HandlerThreadImageLoader. Bien que la plupart du code qui nous intéresse se trouve dans cette même classe, je vous encourage vivement à jeter un oeil au code de l'application pour mieux comprendre comme utiliser le HandlerThreadImageLoader.

HandlerThreadImageLoader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package com.cyrilmottier.android.asyncprogdemo;

import java.lang.ref.WeakReference;
import java.net.URL;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;

/**
 * Simple HandlerThread permettant de télécharger des images en tâche de fond.
 * 
 * @author Cyril Mottier
 */
public class HandlerThreadImageLoader extends HandlerThread implements ImageLoader {

    private static final String LOG_TAG = HandlerThreadImageLoader.class.getSimpleName();

    /**
     * Identifiants "what" utilisés par le Handler s'occupant de faire les
     * appels aux callbacks dans le thread principal
     */
    private static final int DOWNLOAD_STARTED = 1;
    private static final int DOWNLOAD_FAILED = 2;
    private static final int DOWNLOAD_ENDED = 4;

    /**
     * Identifiant "what" utilisé par le Handler associé au Looper en tâche de
     * fond.
     */
    private static final int MESSAGE_DOWNLOAD = 1;

    /**
     * Wrapper permettant de faire passer de l'information à travers les
     * Messages.
     * 
     * @author Cyril Mottier
     */
    private static class LoaderInfo {
        public String url;
        public WeakReference<imageloadercallback> callback;
        public Object obj;

        public LoaderInfo(String url, ImageLoaderCallback callback) {
            this.url = url;
            this.callback = callback == null ? null : new WeakReference<imageloadercallback>(callback);
        }
    }

    /**
     * Handler permettant de communiquer avec le "main thread"
     */
    private Handler mMainHandler;

    /**
     * Handler associé au {@link HandlerThread} courant
     */
    private Handler mLoaderHandler;

    public HandlerThreadImageLoader() {
        super(LOG_TAG, Process.THREAD_PRIORITY_BACKGROUND);
        mMainHandler = new Handler(Looper.getMainLooper(), mMainCallback);
    }

    @Override
    protected void onLooperPrepared() {
        super.onLooperPrepared();
        synchronized (this) {
            mLoaderHandler = new Handler(getLooper(), mLoaderCallback);
            notifyAll();
        }
    }

    @Override
    public boolean quit() {
        if (mLoaderHandler != null) {
            mLoaderHandler.removeMessages(MESSAGE_DOWNLOAD);
            mLoaderHandler = null;
        }
        return super.quit();
    };

    @Override
    public void loadImage(String url, ImageLoaderCallback callback) {

        if (!isAlive()) {
            return;
        }

        synchronized (this) {
            while (isAlive() && mLoaderHandler == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }

        if (mLoaderHandler != null) {
            Message message = mLoaderHandler.obtainMessage();
            message.what = MESSAGE_DOWNLOAD;
            message.obj = new LoaderInfo(url, callback);

            mLoaderHandler.sendMessage(message);
        }
    }

    private Callback mLoaderCallback = new Callback() {

        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_DOWNLOAD:

                    final Handler h = mMainHandler;

                    final LoaderInfo loaderInfo = (LoaderInfo) msg.obj;
                    if (loaderInfo == null) {
                        return false;
                    }

                    try {

                        Message.obtain(h, DOWNLOAD_STARTED).sendToTarget();

                        Bitmap bitmap = null;

                        URL imageUrl = new URL(loaderInfo.url);
                        bitmap = BitmapFactory.decodeStream(imageUrl.openStream());

                        if (bitmap == null) {
                            throw new Exception("Unable to decode the image at the given URL");
                        }

                        // Cas OK
                        loaderInfo.obj = bitmap;
                        Message.obtain(h, DOWNLOAD_ENDED, loaderInfo).sendToTarget();

                    } catch (Exception e) {
                        // Cas d'erreur
                        loaderInfo.obj = e;
                        Message.obtain(h, DOWNLOAD_FAILED, loaderInfo).sendToTarget();
                    }
            }

            return true;
        }
    };

    private Callback mMainCallback = new Callback() {

        @Override
        public boolean handleMessage(Message msg) {

            if (mLoaderHandler == null) {
                // Le Looper a été arrêté. On considère donc que toutes les
                // tâches se terminant sont invalides et ne callback rien.
                return false;
            }

            final LoaderInfo loaderInfo = (LoaderInfo) msg.obj;
            if (loaderInfo == null) {
                return false;
            }

            final ImageLoaderCallback callback = (loaderInfo.callback == null) ? null : loaderInfo.callback.get();
            if (callback == null) {
                return false;
            }

            switch (msg.what) {
                case DOWNLOAD_STARTED:
                    callback.onImageLoadingStarted(HandlerThreadImageLoader.this);
                    break;

                case DOWNLOAD_FAILED:
                    callback.onImageLoadingFailed(HandlerThreadImageLoader.this, (Throwable) loaderInfo.obj);
                    break;

                case DOWNLOAD_ENDED:
                    callback.onImageLoadingEnded(HandlerThreadImageLoader.this, (Bitmap) loaderInfo.obj);
                    break;

                default:
                    return false;
            }

            return true;
        }
    };

}

Optimisations

Exécuter des opérations en tâche de fond ne signifie pas qu'il vous est possible de faire tout et n'importe quoi sans impacter les performances du terminal. Il ne faut jamais oublier que le code que vous écrivez s'exécute sur des machines généralement peu puissantes. A ce titre, voici quelques éléments et optimisations supplémentaires fournis “tel quel”.

A la lecture de cet article, on se rend compte que la classe Message est extrêmement importante. Les instances de cette classe peuvent potentiellement être nombreuses. Pour éviter de créer énormément d'instances (comprendre faire des new Message()), il est possible de réutiliser les Message n'ayant plus d'utilité. Je vous recommande donc vivement de récupérer un Message en faisant appel aux méthodes suivantes :

  • Message.obtain() : Permet de récupérer un Message du pool global ou d'en créer un nouveau si aucun Message n'est disponible dans le pool.

  • Handler.obtainMessage() : Cette méthode est similaire à la précédente mais associe directement le Message au Handler courant.

Beaucoup de gens pensent, à tort, qu'un Service s'exécute dans un Thread secondaire. En réalité, un Service s'exécute, au même titre qu'une Activity ou un BroadcastReceiver dans le main thread. Pour éviter de bloquer inutilement le main thread il peut donc être judicieux de démarrer un HandlerThread. La classe IntentService remplit parfaitement ce rôle :

  • Au premier appel de startService(Intent), le Service démarre et initialise un HandlerThread

  • L'Intent passé à la méthode startService(Intent) est ensuite transféré à la méthode onHandleIntent(Intent) qui s'occupe de traiter la demande exprimée par l'Intent

  • Si aucun autre appel à startService(Intent) n'a été fait avant la fin de onHandleIntent(Intent), le Service s'arrête.

  • Si d'autres appels à startService(Intent) sont effectués, les Intents “s'empilent” et seront gérés lors d'un prochain tour de boucle du Looper

L'IntentService est particulièrement adapté aux requêtes réseau. En effet, il permet de traiter les demandes les unes après les autres et s'arrête de lui même lorsqu'il ne reste plus aucune requêtes à traiter.

Conclusion

Voilà ! Je pense avoir fait le tour de la question des Handlers, Loopers et autre Messages sur Android. N'hésitez pas à utiliser ce système aussi bien utile pour gérer la communication inter-threads que pour retarder l'exécution d'instructions (postDelayed).