Thread, Handler, AsyncTask et fuite mémoire

Cet article est rédigé par Android2EE, Consulting, Expertise et Formation Android.

Il est associé à un ensemble de tutoriels vous montrant comment utiliser ces objets ainsi qu’à deux projets démontrant la réalité des fuites mémoires. Pour plus d’information (Tutoriels, EBooks, Formations), une seule adresse :

Android2EE : http://www.android2ee.com

Introduction

Cet article explique comment mettre en place des Threads de background de manière appropriée sous Android. Il explique comment effectuer des traitements dans vos applications. Le discours se centre sur les activités ayant à effectuer des traitements, mais ces principes s’appliquent aussi bien aux services.

La première partie de cet article explique le fonctionnement des Handlers et des AsyncTasks de manière générale. Elle montre comment déclarer une Thread de traitement et la faire communiquer avec votre Thread d’IHM.

La seconde partie se concentre sur la problématique des fuites mémoires qui apparaissent si la Thread n’est pas liée au cycle de vie de l’activité (du service). Et oui, je ne vais considérer les fuites mémoires que dans le cadre de la synchronisation du cycle de vie des Threads et des activités. Cet article ne traite pas des fuites mémoires dans leur généralité.

Les Threads

Il faut toujours garder en tête quand on fait de la programmation Android que si une activité réagit en plus de 5 secondes, elle sera tuée par l’ActivityManager qui la considèrera comme morte.

Les IHMs de l’activité sont dans une Thread qui lui est propre (comme en swing). C’est cette Thread qui est chargée de l’interaction avec l’utilisateur. Ainsi pour effectuer un traitement, il faut lancer une autre Thread (dites de background, de traitement ou d’arrière-plan) qui effectue le traitement. Plusieurs façons d’interagir entre les Threads en arrière-plan et la Thread d’IHM sont possibles.

Mais surtout il faut garder  toujours en tête qu’aucun traitement ne doit être effectué dans la Thread d’IHM !

Deux objets dédiés à ce pattern sont disponibles nativement sur Android, les Handlers et les AsyncTasks.

Les Handlers

Le Handler est associé à l’activité qui le déclare et travaille au sein de la Thread d’IHM. Ce qui signifie que tout traitement effectué par le Handler gèle l’IHM le temps qu’il soit effectué. Il faut donc considérer le Handler comme celui qui met à jour l’IHM, la Thread qui appelle le Handler a la charge du traitement. Le Handler ne doit que mettre à jour l’IHM, tout autre comportement est une erreur de conception.

Une Thread communique avec un Handler au moyen de messages (de l’objet Message pour être exact). Pour cela :

·         La Thread récupère l’objet Message du pool du Handler par handler.obtainedMessage. Cette méthode peut être surchargée de manière à envoyer plus d’informations à la Thread (en lui passant d’autres paramètres au moyen d’un Bundle[1]).

·         La Thread envoie le message au Handler en utilisant l’une des méthodes suivantes :

o   sendMessage (envoie le message et le place à la fin de la queue)

o   sendMessageAtFrontOfQueue (envoie le message et le place au début de la queue)

o   sendMessageAtTime (envoie le message au moment donné en paramètre et le place à la fin de la queue)

o   sendMessageDelayed (envoie le message après un temps d’attente passé en paramètre et le place à la fin de la queue)

·         Le Handler doit surcharger sa méthode handleMessage pour répondre aux messages qui lui sont envoyés. Il a à charge de mettre à jour l’IHM en fonction de ces données.

 

Handler2.JPG

 

L’exemple suivant met à jour une ProgressBar:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

            android:orientation="vertical"

            android:layout_width="fill_parent"

            android:layout_height="fill_parent"

            >

            <ProgressBar android:id="@+id/progress"

                        style="?android:attr/progressBarStyleHorizontal"

                        android:layout_width="fill_parent"

                        android:layout_height="wrap_content" />

</LinearLayout>

Le code java :

/**

 * @author Android2ee

 * @goals Cette classe a pour but de montrer comment utiliser les Handlers et les Threads

 */

public class HandlerTuto extends Activity {

    /**     * La ProgressBar à mettre à jour     */

    ProgressBar bar;

    /**     * La clef dans le bundle pour incrémenter la progressBar     */

    private final String PROGRESS_BAR_INCREMENT="ProgreesBarIncrementId";

    /**     * Le Handler à charge de la communication entre la Thread de background et celle de l’IHM     */

    Handler handler = new Handler() {

        @Override

        public void handleMessage(Message msg) {

            int progress=msg.getData().getInt(PROGRESS_BAR_INCREMENT);

            // Incrémenter la ProgressBar, on est bien dans la Thread de l’IHM

            bar.incrementProgressBy(progress);

            // On peut faire toute action qui met à jour l’IHM

           

        }

    };

    /**     * L’AtomicBoolean qui gère la destruction de la Thread de background     */

    AtomicBoolean isRunning = new AtomicBoolean(false);

    /**     * L’AtomicBoolean qui gère la mise en pause de la Thread de background     */

    AtomicBoolean isPausing = new AtomicBoolean(false);

 

    // Création de l’activité

    @Override

    public void onCreate(Bundle icicle) {

        super.onCreate(icicle);

        setContentView(R.layout.main);

        // Définition de la ProgressBar

        bar = (ProgressBar) findViewById(R.id.progress);

        bar.setMax(210);

    }

 

    /** Méthode du cycle de vie de l’activité (lancement de celle-ci) */

    public void onStart() {

        super.onStart();

        // initialisation de la ProgressBar

        bar.setProgress(0);

        // Définition de la Thread (peut être effectuée dans une classe externe ou interne)

        Thread background = new Thread(new Runnable() {

            /**

             * Le Bundle qui porte les données du Message et sera transmis au Handler

             */

            Bundle messageBundle=new Bundle();

            /**

             * Le message échangé entre la Thread et le Handler

             */

            Message myMessage;

            // Surcharge de la méthode run

            public void run() {

                try {

                    // Si isRunning est à false, la méthode run doit s’arrêter

                    for (int i = 0; i < 20 && isRunning.get(); i++) {

                        // Si l’activité est en pause mais pas morte

                        while (isPausing.get() && (isRunning.get())) {

                            // Faire une pause ou un truc qui soulage le CPU (dépend du traitement)

                            Thread.sleep(2000);

                        }

                        // Effectuer le traitement, pour l’exemple je dors une seconde

                        Thread.sleep(1000);

                        // Envoyer le message au Handler (la méthode handler.obtainMessage est plus efficace

                        // que créer un message à partir de rien, optimisation du pool de message du Handler)

                        //Instanciation du message (la bonne méthode):

                        myMessage=handler.obtainMessage();   

                        //Ajouter des données à transmettre au Handler via le Bundle

                        messageBundle.putInt(PROGRESS_BAR_INCREMENT, 1);

                       //Ajouter le Bundle au message

                        myMessage.setData(messageBundle);

                        //Envoyer le message

                        handler.sendMessage(myMessage);

                    }

                } catch (Throwable t) {

                    // gérer l’exception et arrêter le traitement

                }

            }

        });

        //Initialisation des AtomicBooleans

        isRunning.set(true);

        isPausing.set(false);

        //Lancement de la Thread

        background.start();

    }

   

 /************************************************************************************/

/** Gestion du cycle de vie *******************************************************************/

 /**************************************************************************************/

    //Méthode appelée quand l’activité s’arrête

    public void onStop() {

        super.onStop();

        //Mise-à-jour du booléen pour détruire la Thread de background

        isRunning.set(false);

    }

 

    /* (non-Javadoc)

     * @see android.app.Activity#onPause()

     */

    @Override

    protected void onPause() {

        super.onPause();

        // Mise-à-jour du booléen pour mettre en pause la Thread de background

        isPausing.set(true);

    }

 

    /* (non-Javadoc)

     * @see android.app.Activity#onResume()

     */

    @Override

    protected void onResume() {

        super.onResume();

        // Mise-à-jour du booléen pour relancer la Thread de background

        isPausing.set(false);

    }

 

}

 

Cet exemple utilise des objets de type AtomicBoolean, l’explication est donnée dans le paragraphe traitant des fuites mémoires.

Suis-je dans la Thread de l’IHM ou pas ?

La classe Activity possède la méthode runOnUIThread pour lancer un traitement dans la Thread de l’IHM.

La désynchronisation avec l’AsyncTask

Depuis la version 1.5 d’Android, l’AsyncTask permet une nouvelle façon d’effectuer des tâches en arrière-plan.

Pour cela :

·         Créer une sous-classe d’AsyncTask (elle peut être interne et privée à l’activité).

·         Redéfinir une ou plusieurs de ces méthodes pour spécifier son travail.

·         La lancer au moyen de sa méthode execute

La dérivation d’une classe AsyncTask n’est pas triviale, elle est générique et à paramètres variables. Pour la généricité elle attend 3 paramètres :

·         Le type de l’information qui est nécessaire au traitement (dans l’exemple URL)

·         Le type de l’information qui est passé à sa tâche pour indiquer sa progression (dans l’exemple : Integer)

·         Le type de l’information passé au code lorsque la tâche est finie (dans l’exemple Long).

Une dernière chose d’importance est que la classe AsyncTask (et donc vos classes filles) s’exécute dans deux Threads distinctes ; la Thread d’IHM et la Thread de traitement.

 

Un exemple (toujours celui qui met à jour une ProgressBar):

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {

    protected Long doInBackground(URL... urls) {

        int count = urls.length;

        long totalSize = 0;

        for (int i = 0; i < count; i++) {

            totalSize += Downloader.downloadFile(urls[i]);

            publishProgress((int) ((i / (float) count) * 100));

        }

        return totalSize;

    }

 

    protected void onProgressUpdate(Integer... progress) {

        setProgressPercent(progress[0]);

    }

 

    protected void onPostExecute(Long result) {

        showDialog("Downloaded " + result + " bytes");

    }

}

 

Les étapes d’AsynchTask :

doInBackground est la méthode qui s’exécute dans une autre Thread. Elle reçoit un tableau d’objets lui permettant ainsi d’effectuer un traitement en série sur ces objets. Seule cette méthode est exécutée dans une Thread à part, les autres méthodes s’exécutent dans la Thread de l’IHM.

onPreExecute est appelée par la Thread de l’IHM avant l’appel à doInBackground, elle permet de pré-initialiser les éléments de l’IHM.

onPostExecute est appelée lorsque la méthode doInBackground est terminée.

onProgressUpdate est appelée par la méthode publishProgress à l’intérieur de la méthode doInBackground.

 

AsyncTask.JPG

Exemple :

 

/**

 * @author Android2ee

 * @goals Cette classe montre un usage simple du Pattern AsyncTask. Attention, elle génère des fuites mémoires

 *  si elle est utilisée telle quelle.

 */

public class AsyncTuto extends Activity {

    /**    * La ProgressBar à mettre à jour     */

    ProgressBar bar;

 

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        // Définition de la ProgressBar

        bar = (ProgressBar) findViewById(R.id.progress);

        bar.setMax(210);

        // Lancement de l’AsynchTask

        new MyAsyncTask().execute();

    }

 

    /**

     * Cette classe montre une simple dérivation de la classe AsyncTask

     */

    class MyAsyncTask extends AsyncTask<Void, Integer, String> {

        /**     * Un compteur */

        Integer count = 0;

 

        // Surcharge de la méthode doInBackground (Celle qui s’exécute dans une Thread à part)

        @Override

        protected String doInBackground(Void... unused) {

            try {

                while (count < 20) {

                    //incrémente le compteur

                    count++;

                    // Faire une pause

                    Thread.sleep(1000);

                    //Donne son avancement en appelant onProgressUpdate

                    publishProgress(count);

                }

            } catch (InterruptedException t) {

                // Gérer l’exception et terminer le traitement

                return ("The sleep operation failed");

            }

            return ("return object when task is finished");

        }

 

        // Surcharge de la méthode onProgressUpdate (s’exécute dans la Thread de l’IHM)

        @Override

        protected void onProgressUpdate(Integer... diff) {

             // Mettre à jour l’IHM

            bar.incrementProgressBy(diff[0]);

        }

 

        // Surcharge de la méthode onPostExecute (s’exécute dans la Thread de l’IHM)

        @Override

        protected void onPostExecute(String message) {

             // Mettre à jour l’IHM

            Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();

        }

    }

}

 

AsyncTask ou Handler

Ces deux méthodes ont pour but d’effectuer des traitements liés aux IHMs dans des Threads indépendantes de celle de l’IHM. La question est de savoir laquelle choisir dans quelles circonstances.

 

L’idée est que si vous faites un traitement lourd, spécifique, qui n’a besoin que de donner son avancement et sa fin (typiquement charger des données), le mieux est l’utilisation de l’AsyncTask.

 

A contrario, si vous avez besoin d’établir une communication avec l’IHM, il est plus pertinent d’utiliser un Handler ( par exemple la communication Bluetooth entre deux appareils utilise ce pattern).

 

Ainsi, si vous avez un traitement qui correspond à effectuer la même action sur un ensemble d’éléments donnés et connus avant le début du traitement, l’utilisation de l’AsyncTask est préconisée. Sinon, il y a de grandes chances qu’il vous faille utiliser le pattern du Handler.

 

En conclusion: un traitement par lots s'effectuera avec une AsyncTask et un traitement nécessitant une communication dynamique entre la Thread de traitement et l'IHM utilisera le pattern du Handler.

Threads et fuites mémoires

 La question qui se pose est « Que se passe-t-il lorsque l’activité suit son cycle de vie et est détruite puis recréée ? Typiquement une rotation de l’écran? Que devient la Thread de traitement ? »

Dans la plupart des cas, le développeur ne fait pas attention et l’ancienne Thread n’est pas détruite, une nouvelle Thread est mise en place. Le traitement peut rester cohérent (ou pas).

La même chose se produit avec les AsynchTask bien que seuls les Handlers soient cités dans la suite de l’article. Par contre, les tutoriels disponibles traitent tous les cas.

Le schéma mémoire qui se met en place est alors le suivant :

HandlerMemoryLeak2.JPG

On s’aperçoit que la destruction de l’activité ne détruit pas la Thread, celle-ci reste « en vie ». Pire, elle continue de pointer vers le Handler et l’activité, qui bien que considérés détruits par Android ne le sont pas (vous ne pouvez pas y accéder, ni afficher l’activité…). Le GarbageCollector détecte que ces ressources sont utilisées (en effet la Thread pointe sur ces espaces mémoires) et ne les collecte pas.

Pire encore, quand l’activité est recréée (typiquement un changement d’orientation),  elle recrée aussi une nouvelle Thread et la lance.

On se retrouve au final (suite à un changement d’orientation par exemple) avec :

·         deux threads qui effectuent le même traitement mais n’en sont pas au même point,

·         Une Activity et un Handler actifs,

·         Une activité et un Handler fantômes, ni vraiment morts ni vraiment vivants.

Alors quelle est la parade ?

Avant de trouver la solution, il faut tout d’abord se poser les questions suivantes :

·         La Thread doit-elle être détruite lorsque l’activité se termine ?

·         La Thread doit-elle se terminer lorsque l’activité est détruite puis recréée ?

·         L’utilisateur a-t-il son mot à dire ?

Ces questions sont fondamentales pour le Design Pattern que vous allez implémenter pour votre traitement.

Clairement si la fin de l’activité ne termine pas la Thread, il est fort à parier que vous auriez du mettre un service entre l’activité et la Thread. Celui-ci ayant à charge le traitement est lancé par l’activité et continue indépendamment du cycle de vie de l’activité.

Dans les cas où le cycle de vie de votre activité doit être lié à celui de votre Thread, deux cas s’offrent à vous : l’utilisation de deux booléens « synchronized » appelés AtomicBoolean ou l’utilisation en plus de ces deux éléments de la méthode onRetainNonConfigurationInstance.

Deux projets Eclipse démontrant cette fuite mémoire sont disponible ici : page des tutoriels sur les Handlers.

L’un démontre cette fuite avec un Handler, l’autre la démontre avec un AsyncTask.

Utilisation des AtomicBooleans pour lier le cycle de vie de la Thread à celui de l’activité.

Les AtomicBooleans sont des objets de type Boolean qui sont thread safe. En effet les méthodes d’accès aux valeurs du booléens sont « synchronized ». Ainsi on accède à la valeur de ce type d’objet par appel à la méthode get() et on change sa valeur en utilisant la méthode set.

public class HandlerTutoActivity extends Activity {

/*****************************************************************************************/

/** Gérer le Handler et la Thread *************************************************/

/*****************************************************************************************/

/** * Le Handler */

private Handler handler;

/** * Le AtomicBoolean pour lancer et stopper la Thread */

private AtomicBoolean isThreadRunnning = new AtomicBoolean();

/** * Le AtomicBoolean pour mettre en pause et relancer la Thread */

private AtomicBoolean isThreadPausing = new AtomicBoolean();

/** * La Thread */

Thread backgroundThread;

 

/****************************************************************************************/

/** Gérer l’activité ********************************************************************/

/***************************************************************************************/

 

/** Appelé à la création de l’activité. */

@Override

public void onCreate(Bundle savedInstanceState) {

            super.onCreate(savedInstanceState);

            setContentView(R.layout.main);

            // Instancier la ProgressBar

            progressBar = (ProgressBar) findViewById(R.id.progressbar);

            progressBar.setMax(100);

            // définition du Handler

            handler = new Handler() {

                        /* * (non-Javadoc) *

                         * @see android.os.Handler#handleMessage(android.os.Message) */

                        @Override

                        public void handleMessage(Message msg) {

                                   super.handleMessage(msg);

                                   Log.d(TAG, "handle message called ");

                                   // s’assurer que la Thread est bien en mode Run avant de faire quelque chose

                                   if (isThreadRunnning.get()) {

                                               Log.w(TAG, "handle message calls updateProgress ");

                                               updateProgress();

                                   }

                        }

            };

            // Définition de la Thread et liaison avec le Handler

            backgroundThread = new Thread(new Runnable() {

                        /**

                         * Le message échangé entre la Thread et le Handler

                         */

                        Message myMessage;

 

                        /** (non-Javadoc) * * @see java.lang.Runnable#run()*/

                        public void run() {

                                   try {

                                   while (isThreadRunnning.get()) {

                                               if (isThreadPausing.get()) {

                                                           // Faire une pause cohérente avec le traitement, soulager le CPU

                                                           Thread.sleep(2000);

                                               } else {

                                                           // Faire un traitement

                                                           Thread.sleep(100);

                                                           // Obtenir le Message

                                                           myMessage = handler.obtainMessage();

                                                           // envoyer le message au Hanlder

                                                           handler.sendMessage(myMessage);

                                               }

                                   }

                                   } catch (Throwable t) {

                                               // Termine la Thread

                                   }

                        }

            });

            // Initialiser le booléen isThreadRunning

            isThreadRunnning.set(true);

            //Lancer la Thread

            backgroundThread.start();

}

 

/* * (non-Javadoc) * * @see android.app.Activity#onDestroy() */

protected void onDestroy() {

            // Tuer la Thread

            isThreadRunnning.set(false);

            super.onDestroy();

}

 

/* * (non-Javadoc) *  * @see android.app.Activity#onPause() */

protected void onPause() {

            //Mettre la Thread en pause

            isThreadPausing.set(true);

            super.onPause();

}

 

/* * (non-Javadoc) *  * @see android.app.Activity#onResume() */

protected void onResume() {

            // Relancer la Thread

            isThreadPausing.set(false);

            super.onResume();

}

Ce pattern (qui est le même que celui présenté en début d’article) permet d’obtenir le schéma mémoire suivant lors du passage de l’activité par les méthodes onDestroy, onCreate :

HanlderMemoryWithAtomic2.JPG

Ce schéma mémoire a le mérite d’être sans fuite mémoire, a contrario, il réinitialise le traitement de la Thread (en fait il en créé un nouvelle) à chaque redémarrage de l’activité. Il pourrait être utile de sauvegarder l’état d’avancement de la Thread dans la méthode onDestroy puis de restaurer cet état dans la méthode onCreate.

Cette solution ne permet pas de pallier au problème du changement de configuration de l’appareil qui reconstruit l’activité immédiatement après sa destruction. Pour cela il faut utiliser la méthode onRetainNonConfigurationInstance.

Un tutoriel présentant cette solution est disponible ici : page des tutoriels sur les Handlers.

Utilisation de la méthode onRetainNonConfigurationInstance

Attention : Pour garder un caractère compréhensible, ce paragraphe explique l’utilisation de cette méthode. Mais si vous utilisez le code décrit ci-dessous directement vous ne gérez plus la fuite mémoire générée par la destruction de l’activité. Il faut inclure dans la solution de ce paragraphe, l’utilisation des AtomicBoolean du paragraphe précédent qui permettent de n’avoir jamais de fuite mémoire. Le tutoriel HandlerActivityBindingThreadTuto disponible sur Android2ee vous présente la solution complète et propre, il se télécharge ici : page des tutoriels sur les Handlers.

Mais tout d’abord « Qu'es aquò la méthode OnRetainNonConfigurationInstance ? » comme dirait mon grand-père.

De manière générale les méthodes Object public getLastNonConfigurationInstance() et public Object onRetainNonConfigurationInstance() permettent de passer un objet entre deux instances d’une activité lorsque celle-ci est détruite pour être immédiatement recréée. Il faut donc toujours tester la valeur de retour (null ou pas) et être cohérent avec ce retour.

On renvoie l’objet (ou la liste d’objets) avec la méthode onRetainNonConfiguartionInstance et on le récupère avec getLastNonConfigurationInstance.

Nous utiliserons ces méthodes pour passer en paramètre la Thread ; ce qui donne :

/** Appelée à la première creation de l’activité */

@Override

public void onCreate(Bundle savedInstanceState) {

                super.onCreate(savedInstanceState);

//Faire quelque chose

                backgroundThread = (Thread) getLastNonConfigurationInstance();

//Tester si la Thread est nulle ou pas et adapter le traitement en fonction

}

@Override

public Object onRetainNonConfigurationInstance() {

                //Renvoyer la Thread

                return backgroundThread;

}

 

Ce DesignPattern permet d’obtenir le schéma mémoire suivant :

HanlderMemoryWithOnRetain2.JPG

Remarquez que ce schéma mémoire a le bon goût lors de la destruction puis recréation (immédiate) de l’activité de conserver la même Thread de Traitement et d’éviter les fuites mémoires. Il est à noter que sans les AtomicBooleans lorsque l’activité s’arrête, vous générez une fuite mémoire, il faut donc impérativement mixer les deux approches.

Un tutoriel présentant cette solution est disponible ici : page des tutoriels sur les Handlers.

Conclusion

Suite à cet article vous êtes censés être capable d’effectuer des traitements dans vos applications qui ne gèlent pas vos IHMs et qui ne génèrent pas de fuite mémoire. J’espère que vous vous en souviendrez dans vos projets ou quand vous expliquerez à vos stagiaires comment coder proprement. En attendant, bonne continuation à vous.

 



[1] Un Bundle n’est autre qu’une HashMap typée. Une HashMap est un objet qui stocke des éléments de type (String Clef, Object Valeur). Ici, elle est typée donc on a des méthodes du type getInteger, putInteger, getSring… qui permettent de spécifier le type de la valeur stockée.