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
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é.
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.
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.
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>
/**
* @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.
La classe Activity possède la
méthode runOnUIThread pour lancer un traitement dans
la Thread de l’IHM.
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.
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();
}
}
}
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.
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 :
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.
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 :
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.
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 :
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.
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.