Tout comprendre sur les Threads, les Handlers, les AsyncTasks et les fuites mémoires

Android2ee

Mathias Seguy

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.

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum : Commentez ♪

- Titre : Tout comprendre sur les Threads, les Handlers, les AsyncTasks et les fuites mémoires

- Auteur : Mathias Seguy

- Parution : 27 janvier 2012

- Mise à jour : 4 janvier 2016

- Licence : Copyright ® 2016 Mathias Seguy. 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

- I. Introduction

- I-A. Les Threads

- II. Les Handlers

- II-A. Suis-je dans la Thread de l'IHM ou pas ?

- III. La désynchronisation avec l'AsyncTask

- IV. AsyncTask ou Handler

- V. Threads et fuites mémoire

- V-A. Utilisation des AtomicBooleans pour lier le cycle de vie de la Thread à celui de l'activité

- V-B. Utilisation de la méthode onRetainNonConfigurationInstance

- VI. Conclusion

- VII. Remerciements

I - 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émoire qui apparaissent si la Thread n'est pas liée au cycle de vie de l'activité (du service). Eh oui, je ne vais considérer les fuites mémoire 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émoire dans leur généralité.

I-A - 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 (dite 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.

II - 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) ;

- la Thread envoie le message au Handler en utilisant l'une des méthodes suivantes :

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

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

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

- 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 un composant de type ProgressBar :

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

2. android:orientation="vertical"

3. android:layout_width="fill_parent"

4. android:layout_height="fill_parent"

5. >

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

7. style="?android:attr/progressBarStyleHorizontal"

8. android:layout_width="fill_parent"

9. android:layout_height="wrap_content" />

10. </LinearLayout>

Le code java :

1. /**

2. * @author Android2ee

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

4. */

5. public class HandlerTuto extends Activity {

6. /**     * La ProgressBar à mettre à jour     */

7. ProgressBar bar;

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

9. private final String PROGRESS_BAR_INCREMENT="ProgreesBarIncrementId";

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

11. Handler handler = new Handler() {

12. @Override

13. public void handleMessage(Message msg) {

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

15. // Incrémenter la ProgressBar, on est bien dans la Thread de l'IHM

16. bar.incrementProgressBy(progress);

17. // On peut faire toute action qui met à jour l'IHM

19. }

20. };

21. /**     * L'AtomicBoolean qui gère la destruction de la Thread de background     */

22. AtomicBoolean isRunning = new AtomicBoolean(false);

23. /**     * L'AtomicBoolean qui gère la mise en pause de la Thread de background     */

24. AtomicBoolean isPausing = new AtomicBoolean(false);

26. // Création de l'activité

27. @Override

28. public void onCreate(Bundle icicle) {

29. super.onCreate(icicle);

30. setContentView(R.layout.main);

31. // Définition de la ProgressBar

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

33. bar.setMax(210);

34. }

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

37. public void onStart() {

38. super.onStart();

39. // initialisation de la ProgressBar

40. bar.setProgress(0);

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

42. Thread background = new Thread(new Runnable() {

43. /**

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

45. */

46. Bundle messageBundle=new Bundle();

47. /**

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

49. */

50. Message myMessage;

51. // Surcharge de la méthode run

52. public void run() {

53. try {

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

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

56. // Si l'activité est en pause, mais pas morte

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

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

59. Thread.sleep(2000);

60. }

61. // Effectuer le traitement, pour l'exemple je dors une seconde

62. Thread.sleep(1000);

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

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

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

66. myMessage=handler.obtainMessage();

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

68. messageBundle.putInt(PROGRESS_BAR_INCREMENT, 1);

69. //Ajouter le Bundle au message

70. myMessage.setData(messageBundle);

71. //Envoyer le message

72. handler.sendMessage(myMessage);

73. }

74. } catch (Throwable t) {

75. // gérer l'exception et arrêter le traitement

76. }

77. }

78. });

79. //Initialisation des AtomicBooleans

80. isRunning.set(true);

81. isPausing.set(false);

82. //Lancement de la Thread

83. background.start();

84. }

86. /************************************************************************************/

87. /** Gestion du cycle de vie *******************************************************************/

88. /**************************************************************************************/

89. //Méthode appelée quand l'activité s'arrête

90. public void onStop() {

91. super.onStop();

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

93. isRunning.set(false);

94. }

96. /* (non-Javadoc)

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

98. */

99. @Override

100. protected void onPause() {

101. super.onPause();

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

103. isPausing.set(true);

104. }

106. /* (non-Javadoc)

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

108. */

109. @Override

110. protected void onResume() {

111. super.onResume();

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

113. isPausing.set(false);

114. }

116. }

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

II-A - 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.

III - 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 trois 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 un composant de type ProgressBar) :

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

2. protected Long doInBackground(URL... urls) {

3. int count = urls.length;

4. long totalSize = 0;

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

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

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

8. }

9. return totalSize;

10. }

12. protected void onProgressUpdate(Integer... progress) {

13. setProgressPercent(progress[0]);

14. }

16. protected void onPostExecute(Long result) {

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

18. }

19. }

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 :

1. /**

2. * @author Android2ee

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

4. *  si elle est utilisée telle quelle.

5. */

6. public class AsyncTuto extends Activity {

7. /**    * La ProgressBar à mettre à jour     */

8. ProgressBar bar;

10. @Override

11. public void onCreate(Bundle savedInstanceState) {

12. super.onCreate(savedInstanceState);

13. setContentView(R.layout.main);

14. // Définition de la ProgressBar

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

16. bar.setMax(210);

17. // Lancement de l'AsynchTask

18. new MyAsyncTask().execute();

19. }

21. /**

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

23. */

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

25. /**     * Un compteur */

26. Integer count = 0;

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

29. @Override

30. protected String doInBackground(Void... unused) {

31. try {

32. while (count < 20) {

33. //incrémente le compteur

34. count++;

35. // Faire une pause

36. Thread.sleep(1000);

37. //Donne son avancement en appelant onProgressUpdate

38. publishProgress(count);

39. }

40. } catch (InterruptedException t) {

41. // Gérer l'exception et terminer le traitement

42. return ("The sleep operation failed");

43. }

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

45. }

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

48. @Override

49. protected void onProgressUpdate(Integer... diff) {

50. // Mettre à jour l'IHM

51. bar.incrementProgressBy(diff[0]);

52. }

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

55. @Override

56. protected void onPostExecute(String message) {

57. // Mettre à jour l'IHM

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

59. }

60. }

61. }

IV - 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.

V - Threads et fuites mémoire

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émoire) 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 finalement (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 dû 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 disponibles ici :

- OrphanAsyncTaskThreadTuto.zip

- OrphanThreadDemo.zip

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

V-A - 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.

1. public class HandlerTutoActivity extends Activity {

2. /*****************************************************************************************/

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

4. /*****************************************************************************************/

5. /** * Le Handler */

6. private Handler handler;

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

8. private AtomicBoolean isThreadRunnning = new AtomicBoolean();

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

10. private AtomicBoolean isThreadPausing = new AtomicBoolean();

11. /** * La Thread */

12. Thread backgroundThread;

14. /****************************************************************************************/

15. /** Gérer l'activité ********************************************************************/

16. /***************************************************************************************/

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

19. @Override

20. public void onCreate(Bundle savedInstanceState) {

21. super.onCreate(savedInstanceState);

22. setContentView(R.layout.main);

23. // Instancier la ProgressBar

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

25. progressBar.setMax(100);

26. // définition du Handler

27. handler = new Handler() {

28. /* * (non-Javadoc) *

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

30. @Override

31. public void handleMessage(Message msg) {

32. super.handleMessage(msg);

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

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

35. if (isThreadRunnning.get()) {

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

37. updateProgress();

38. }

39. }

40. };

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

42. backgroundThread = new Thread(new Runnable() {

43. /**

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

45. */

46. Message myMessage;

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

49. public void run() {

50. try {

51. while (isThreadRunnning.get()) {

52. if (isThreadPausing.get()) {

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

54. Thread.sleep(2000);

55. } else {

56. // Faire un traitement

57. Thread.sleep(100);

58. // Obtenir le Message

59. myMessage = handler.obtainMessage();

60. // envoyer le message au Hanlder

61. handler.sendMessage(myMessage);

62. }

63. }

64. } catch (Throwable t) {

65. // Termine la Thread

66. }

67. }

68. });

69. // Initialiser le booléen isThreadRunning

70. isThreadRunnning.set(true);

71. //Lancer la Thread

72. backgroundThread.start();

73. }

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

76. protected void onDestroy() {

77. // Tuer la Thread

78. isThreadRunnning.set(false);

79. super.onDestroy();

80. }

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

83. protected void onPause() {

84. //Mettre la Thread en pause

85. isThreadPausing.set(true);

86. super.onPause();

87. }

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

90. protected void onResume() {

91. // Relancer la Thread

92. isThreadPausing.set(false);

93. super.onResume();

94. }

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ée une 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 le 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.

V-B - Utilisation de la méthode onRetainNonConfigurationInstance

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.

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 getLastNonConfigurationInstance() et 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 :

1. /** Appelée à la première création de l'activité */

2. @Override

3. public void  onCreate(Bundle savedInstanceState) {

4. super.onCreate(savedInstanceState);

5. //Faire quelque chose

6. backgroundThread = (Thread) getLastNonConfigurationInstance();

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

8. }

9. @Override

10. public Object onRetainNonConfigurationInstance() {

11. //Renvoyer la Thread

12. return backgroundThread;

13. }

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émoire. 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.

VI - Conclusion

Suite à cet article, vous êtes censé être capable d'effectuer des traitements dans vos applications qui ne gèlent pas vos IHM 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.

VII - Remerciements

J'adresse ici tous mes remerciements à Feanorin pour son implication, son aide et sa sympathie et à Erielle pour l'excellence de ses corrections orthographiques.

Je remercie les correcteurs techniques MrDuChnok, David55 et Feanorin pour la pertinence de leurs remarques.