L'un de vos objectifs prioritaires sera de travailler sur la réactivité de vos applications, c'est-à-dire de faire en sorte qu'elles ne semblent pas molles ou ne se bloquent pas sans raison apparente pendant une durée significative. En effet, l'utilisateur est capable de détecter un ralentissement s'il dure plus de 100 ms, ce qui est un laps de temps très court. Pis encore, Android lui-même peut déceler quand votre application n'est pas assez réactive, auquel cas il lancera une boîte de dialogue qui s'appelle ANR et qui incitera l'utilisateur à quitter l'application. Il existe deux évènements qui peuvent lancer des ANR :
- L'application ne répond pas à une impulsion de l'utilisateur sur l'interface graphique en moins de cinq secondes.
- Un
Broadcast Receiver
s'exécute en plus de dix secondes.
C'est pourquoi nous allons voir ici comment faire exécuter du travail en arrière-plan, de façon à exécuter du code en parallèle de votre interface graphique, pour ne pas la bloquer quand on veut effectuer de grosses opérations qui risqueraient d'affecter l'expérience de l'utilisateur.
La gestion du multitâche par Android
Comme vous le savez, un programme informatique est constitué d'instructions destinées au processeur. Ces instructions sont présentées sous la forme d'un code, et lors de l'exécution de ce code, les instructions sont traitées par le processeur dans un ordre précis.
Tous les programmes Android s'exécutent dans ce qu'on appelle un processus. On peut définir un processus comme une instance d'un programme informatique qui est en cours d'exécution. Il contient non seulement le code du programme, mais aussi des variables qui représentent son état courant. Parmi ces variables s'en trouvent certaines qui permettent de définir la plage mémoire qui est mise à la disposition du processus.
Pour être exact, ce n'est pas le processus en lui-même qui va exécuter le code, mais l'un de ses constituants. Les constituants destinés à exécuter le code s'appellent des threads (« fils d'exécution » en français). Dans le cas d'Android, les threads sont contenus dans les processus. Un processus peut avoir un ou plusieurs threads, par conséquent un processus peut exécuter plusieurs portions du code en parallèle s'il a plusieurs threads. Comme un processus n'a qu'une plage mémoire, alors tous les threads se partagent les accès à cette plage mémoire. On peut voir à la figure suivante deux processus. Le premier possède deux threads, le second en possède un seul. On peut voir qu'il est possible de communiquer entre les threads ainsi qu'entre les processus.
Vous vous rappelez qu'une application Android est constituée de composants, n'est-ce pas ? Nous n'en connaissons que deux types pour l'instant, les activités et les receivers. Il peut y avoir plusieurs de ces composants dans une application. Dès qu'un composant est lancé (par exemple au démarrage de l'application ou quand on reçoit un Broadcast Intent
dans un receiver), si cette application n'a pas de processus fonctionnel, alors un nouveau sera créé. Tout processus nouvellement créé ne possède qu'un thread. Ce thread s'appelle le thread principal.
En revanche, si un composant démarre alors qu'il y a déjà un processus pour cette application, alors le composant se lancera dans le processus en utilisant le même thread.
Processus
Par défaut, tous les composants d'une même application se lancent dans le même processus, et d'ailleurs c'est suffisant pour la majorité des applications. Cependant, si vous le désirez et si vous avez une raison bien précise de le faire, il est possible de définir dans quel processus doit se trouver tel composant de telle application à l'aide de la déclaration du composant dans le Manifest. En effet, l'attribut android:process
permet de définir le processus dans lequel ce composant est censé s'exécuter, afin que ce composant ne suive pas le même cycle de vie que le restant de l'application. De plus, si vous souhaitez qu'un composant s'exécute dans un processus différent mais reste privé à votre application, alors rajoutez « : » à la déclaration du nom du processus.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <activity android:name=".SensorsActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".DetailActivity" > </activity> <receiver android:name=".LowBatteryReceiver" android:process=":sdz.chapitreQuatre.process.deux" > <intent-filter> <action android:name="android.intent.action.BATTERY_LOW" /> </intent-filter> </receiver> |
Ici, j'ai un receiver qui va s'enclencher dès que la batterie devient faible. Configuré de cette manière, mon receiver ne pourra démarrer que si l'application est lancée (comme j'ai rajouté « : », seule mon application pourra le lancer) ; cependant, si l'utilisateur ferme l'application alors que le receiver est en route, le receiver ne s'éteindra pas puisqu'il se trouvera dans un autre processus que le restant des composants.
Quand le système doit décider quel processus il doit tuer, pour libérer de la mémoire principalement, il mesure quelle est l'importance relative de chaque processus pour l'utilisateur. Par exemple, il sera plus enclin à fermer un processus qui ne contient aucune activité visible pour l'utilisateur, alors que d'autres ont des composants qui fonctionnent encore — une activité visible ou un receiver qui gère un évènement. On dit qu'une activité visible a une plus grande priorité qu'une activité non visible.
Threads
Quand une activité est lancée, le système crée un thread principal dans lequel s'exécutera l'application. C'est ce thread qui est en charge d'écouter les évènements déclenchés par l'utilisateur quand il interagit avec l'interface graphique. C'est pourquoi le second nom du thread principal est thread UI (UI pour User Interface, « interface utilisateur » en français).
Mais il est possible d'avoir plusieurs threads. Android utilise un pool de threads (comprendre une réserve de threads, pas une piscine de threads ) pour gérer le multitâche. Un pool de threads comprend un certain nombre n de threads afin d'exécuter un certain nombre m de tâches (n et m n'étant pas forcément identiques) qui se trouvent dans un autre pool en attendant qu'un thread s'occupe d'elles. Logiquement, un pool est organisé comme une file, ce qui signifie qu'on empile les éléments les uns sur les autres et que nous n'avons accès qu'au sommet de cet empilement. Les résultats de chaque thread sont aussi placés dans un pool de manière à pouvoir les récupérer dans un ordre cohérent. Dès qu'un thread complète sa tâche, il va demander la prochaine tâche qui se trouve dans le pool jusqu'à ce qu'il n'y ait plus de tâches.
Avant de continuer, laissez-moi vous expliquer le fonctionnement interne de l'interface graphique. Dès que vous effectuez une modification sur une vue, que ce soit un widget ou un layout, cette modification ne se fait pas instantanément. À la place, un évènement est créé. Il génère un message, qui sera envoyé dans une pile de messages. L'objectif du thread UI est d'accéder à la pile des messages, de dépiler le premier message à traiter, de le traiter, puis de passer au suivant. De plus, ce thread s'occupe de toutes les méthodes de callback du système, par exemple onCreate()
ou onKeyDown()
. Si le système est occupé à un travail intensif, il ne pourra pas traiter les méthodes de callback ou les interactions avec l'utilisateur. De ce fait, un ARN est déclenché pour signaler à l'utilisateur qu'Android n'est pas d'accord avec ce comportement.
De la sorte, il faut respecter deux règles dès qu'on manipule des threads :
- Ne jamais bloquer le thread UI.
- Ne pas manipuler les vues standards en dehors du thread UI.
Enfin, on évite certaines opérations dans le thread UI, en particulier :
- Accès à un réseau, même s'il s'agit d'une courte opération en théorie.
- Certaines opérations dans la base de données, surtout les sélections multiples.
- Les accès fichiers, qui sont des opérations plutôt lentes.
- Enfin, les accès matériels, car certains demandent des temps de chargement vraiment trop longs.
Mais voyons un peu les techniques qui nous permettrons de faire tranquillement ces opérations.
Gérer correctement les threads simples
La base
En Java, un thread est représenté par un objet de type Thread
, mais avant cela laissez-moi vous parler de l'interface Runnable
. Cette interface représente les objets qui sont capables de faire exécuter du code au processeur. Elle ne possède qu'une méthode, void run()
, dans laquelle il faut écrire le code à exécuter.
Ainsi, il existe deux façons d'utiliser les threads. Comme Thread
implémente Runnable
, alors vous pouvez très bien créer une classe qui dérive de Thread
afin de redéfinir la méthode void run()
. Par exemple, ce thread fait en sorte de chercher un texte dans un livre pour le mettre dans un TextView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class ChercherTexte extends Thread { // La phrase à chercher dans le texte public String a_chercher = "Être ou ne pas être"; // Le livre public String livre; // Le TextView dans lequel mettre le résultat public TextView display; public void run() { int caractere = livre.indexOf(a_chercher); display.setText("Cette phrase se trouve au " + caractere + " ème caractère."); } } |
Puis on ajoute le Thread
à l'endroit désiré et on le lance avec synchronized void start ()
:
1 2 3 4 5 6 7 8 | public void onClick(View v) { Thread t = new Thread(); t.livre = hamlet; t.display = v; t.start(); } |
Une méthode synchronized
a un verrou. Dès qu'on lance cette méthode, alors le verrou s'enclenche et il est impossible pour d'autres threads de lancer la même méthode.
Mais ce n'est pas la méthode à privilégier, car elle est contraignante à entretenir. À la place, je vous conseille de passer une instance anonyme de Runnable
dans un Thread
:
1 2 3 4 5 6 7 8 | public void onClick(View v) { new Thread(new Runnable() { public void run() { int caractere = hamlet.indexOf("Être ou ne pas être"); v.setText("Cette phrase se trouve au " + caractere + " ème caractère."); } }).start(); } |
Le problème de notre exemple, c'est que l'opération coûteuse (la recherche d'un texte dans un livre) s'exécute dans un autre thread. C'est une bonne chose, c'est ce qu'on avait demandé, comme ça la recherche se fait sans bloquer le thread UI, mais on remarquera que la vue est aussi manipulée dans un autre thread, ce qui déroge à la seconde règle vue précédemment, qui précise que les vues doivent être manipulées dans le thread UI ! On risque de rencontrer des comportements inattendus ou impossibles à prédire !
Afin de remédier à ce problème, Android offre plusieurs manières d’accéder au thread UI depuis d'autres threads. Par exemple :
- La méthode d'
Activity
void runOnUiThread(Runnable action)
spécifie qu'une action doit s'exécuter dans le thread UI. Si le thread actuel est le thread UI, alors l'action est exécutée immédiatement. Sinon, l'action est ajoutée à la pile des évènements du thread UI. - Sur un
View
, on peut faireboolean post(Runnable action)
pour ajouter leRunnable
à la pile des messages du thread UI. Leboolean
retourné vauttrue
s'il a été correctement placé dans la pile des messages. - De manière presque similaire,
boolean postDelayed(Runnable action, long delayMillis)
permet d'ajouter unRunnable
à la pile des messages, mais uniquement après qu'une certaine duréedelayMillis
s'est écoulée.
On peut par exemple voir :
1 2 3 4 5 6 7 8 9 10 11 12 | public void onClick(View v) { new Thread(new Runnable() { public void run() { int caractere = hamlet.indexOf("Être ou ne pas être"); v.post(new Runnable() { public void run() { v.setText("Cette phrase se trouve au " + caractere + " ème caractère."); } }); } }).start(); } |
Ou :
1 2 3 4 5 6 7 8 9 10 11 12 | public void onClick(View v) { new Thread(new Runnable() { public void run() { int caractere = hamlet.indexOf("Être ou ne pas être"); runOnUiThread(new Runnable() { public void run() { v.setText("Cette phrase se trouve au " + caractere + " ème caractère."); } }); } }).start(); } |
Ainsi, la longue opération s'exécute dans un thread différent, ce qui est bien, et la vue est manipulée dans le thread UI, ce qui est parfait !
Le problème ici est que ce code peut vite devenir difficile à maintenir. Vous avez vu, pour à peine deux lignes de code à exécuter, on a dix lignes d'enrobage ! Je vais donc vous présenter une solution qui permet un contrôle total tout en étant plus évidente à manipuler.
Les messages et les handlers
La classe Thread
est une classe de bas niveau et en Java on préfère travailler avec des objets d'un plus haut niveau. Une autre manière d'utiliser les threads est d'utiliser la classe Handler
, qui est d'un plus haut niveau.
En informatique, « de haut niveau » signifie « qui s'éloigne des contraintes de la machine pour faciliter sa manipulation pour les humains ».
La classe Handler
contient un mécanisme qui lui permet d'ajouter des messages ou des Runnable
à une file de messages. Quand vous créez un Handler
, il sera lié à un thread, c'est donc dans la file de ce thread-là qu'il pourra ajouter des messages. Le thread UI possède lui aussi un handler et chaque handler que vous créerez communiquera par défaut avec ce handler-là.
Les handlers tels que je vais les présenter doivent être utilisés pour effectuer des calculs avant de mettre à jour l'interface graphique, et c'est tout. Ils peuvent être utilisés pour effectuer des calculs et ne pas mettre à jour l'interface graphique après, mais ce n'est pas le comportement attendu.
Mais voyons tout d'abord comment les handlers font pour se transmettre des messages. Ces messages sont représentés par la classe Message
. Un message peut contenir un Bundle
avec la méthode void setData(Bundle data)
. Mais comme vous le savez, un Bundle, c'est lourd, il est alors possible de mettre des objets dans des attributs publics :
arg1
etarg2
peuvent contenir des entiers.- On peut aussi mettre un
Object
dansobj
.
Bien que le constructeur de Message
soit public, la meilleure manière d'en construire un est d'appeler la méthode statique Message Message.obtain()
ou encore Message Handler.obtainMessage()
. Ainsi, au lieu d'allouer de nouveaux objets, on récupère des anciens objets qui se trouvent dans le pool de messages. Notez que si vous utilisez la seconde méthode, le handler sera déjà associé au message, mais vous pouvez très bien le mettre a posteriori avec void setTarget(Handler target)
.
1 2 3 | Message msg = Handler.obtainMessage(); msg.arg1 = 25; msg.obj = new String("Salut !"); |
Enfin, les méthodes pour planifier des messages sont les suivantes :
boolean post(Runnable r)
pour ajouterr
à la queue des messages. Il s'exécutera sur leThread
auquel est rattaché leHandler
. La méthode renvoietrue
si l'objet a bien été rajouté. De manière alternative,boolean postAtTime(Runnable r, long uptimeMillis)
permet de lancer unRunnable
au momentlongMillis
etboolean postDelayed(Runnable r, long delayMillis)
permet d'ajouter unRunnable
à lancer après un délai dedelayMillis
.boolean sendEmptyMessage(int what)
permet d'envoyer unMessage
simple qui ne contient que la valeurwhat
, qu'on peut utiliser comme un identifiant. On trouve aussi les méthodesboolean sendEmptyMessageAtTime(int what, long uptimeMillis)
etboolean sendEmptyMessageDelayed(int what, long delayMillis)
.- Pour pousser un
Message
complet à la fin de la file des messages, utilisezboolean sendMessage(Message msg)
. On trouve aussiboolean sendMessageAtTime(Message msg, long uptimeMillis)
etboolean sendMessageDelayed(Message msg, long delayMillis)
.
Tous les messages seront reçus dans la méthode de callback void handleMessage(Message msg)
dans le thread auquel est attaché ce Handler
.
1 2 3 4 5 6 | public class MonHandler extends Handler { @Override public void handleMessage(Message msg) { // Faire quelque chose avec le message } } |
Application : une barre de progression
Énoncé
Une utilisation typique des handlers est de les incorporer dans la gestion des barres de progression. On va faire une petite application qui ne possède au départ qu'un bouton. Cliquer dessus lance un téléchargement et une boîte de dialogue s'ouvrira. Cette boîte contiendra une barre de progression qui affichera l'avancement du téléchargement, comme à la figure suivante. Dès que le téléchargement se termine, la boîte de dialogue se ferme et un Toast
indique que le téléchargement est terminé. Enfin, si l'utilisateur s'impatiente, il peut très bien fermer la boîte de dialogue avec le bouton Retour
.
Spécifications techniques
On va utiliser un ProgressDialog
pour afficher la barre de progression. Il s'utilise comme n'importe quelle boîte de dialogue, sauf qu'il faut lui attribuer un style si on veut qu'il affiche une barre de progression. L'attribution se fait avec la méthode setProgressStyle(int style)
en lui passant le paramètre ProgressDialog.STYLE_HORIZONTAL
.
L'état d'avancement sera conservé dans un attribut. Comme on ne sait pas faire de téléchargement, l'avancement se fera au travers d'une boucle qui augmentera cet attribut. Bien entendu, on ne fera pas cette boucle dans le thread principal, sinon l'interface graphique sera complètement bloquée ! Alors on lancera un nouveau thread. On passera par un handler pour véhiculer des messages. On répartit donc les rôles ainsi :
- Dans le nouveau thread, on calcule l'état d'avancement, puis on l'envoie au handler à l'aide d'un message.
- Dans le handler, dès qu'on reçoit le message, on met à jour la progression de la barre.
Entre chaque incrémentation de l'avancement, allouez-vous une seconde de répit, sinon votre téléphone va faire la tête. On peut le faire avec :
1 2 3 4 5 | try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } |
Enfin, on peut interrompre un Thread
avec la méthode void interrupt()
. Cependant, si votre thread est en train de dormir à cause de la méthode sleep
, alors l'interruption InterruptedException
sera lancée et le thread ne s'interrompra pas. À vous de réfléchir pour contourner ce problème.
Ma solution
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 | import android.app.Activity; import android.app.ProgressDialog; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.Toast; public class ProgressBarActivity extends Activity { private final static int PROGRESS_DIALOG_ID = 0; private final static int MAX_SIZE = 100; private final static int PROGRESSION = 0; private Button mProgressButton = null; private ProgressDialog mProgressBar = null; private Thread mProgress = null; private int mProgression = 0; // Gère les communications avec le thread de téléchargement final private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); // L'avancement se situe dans msg.arg1 mProgressBar.setProgress(msg.arg1); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_progress_bar); mProgressButton = (Button) findViewById(R.id.progress_button); mProgressButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Initialise la boîte de dialogue showDialog(PROGRESS_DIALOG_ID); // On remet le compteur à zéro mProgression = 0; mProgress = new Thread(new Runnable() { public void run() { try { while (mProgression < MAX_SIZE) { // On télécharge un bout du fichier mProgression = download(); // Repose-toi pendant une seconde Thread.sleep(1000); Message msg = mHandler.obtainMessage(PROGRESSION, mProgression, 0); mHandler.sendMessage(msg); } // Le fichier a été téléchargé if (mProgression >= MAX_SIZE) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(ProgressBarActivity.this, ProgressBarActivity.this.getString(R.string.over), Toast.LENGTH_SHORT).show(); } }); // Ferme la boîte de dialogue mProgressBar.dismiss(); } } catch (InterruptedException e) { // Si le thread est interrompu, on sort de la boucle de cette manière e.printStackTrace(); } } }).start(); } }); } @Override public Dialog onCreateDialog(int identifiant) { if(mProgressBar == null) { mProgressBar = new ProgressDialog(this); // L'utilisateur peut annuler la boîte de dialogue mProgressBar.setCancelable(true); // Que faire quand l'utilisateur annule la boîte ? mProgressBar.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { // On interrompt le thread mProgress.interrupt(); Toast.makeText(ProgressBarActivity.this, ProgressBarActivity.this.getString(R.string.canceled), Toast.LENGTH_SHORT).show(); removeDialog(PROGRESS_DIALOG_ID); } }); mProgressBar.setTitle("Téléchargement en cours"); mProgressBar.setMessage("C'est long..."); mProgressBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); mProgressBar.setMax(MAX_SIZE); } return mProgressBar; } public int download() { if(mProgression <= MAX_SIZE) { mProgression++; return mProgression; } return MAX_SIZE; } } |
Sécuriser ses threads
Les threads ne sont pas des choses aisées à manipuler. À partir de notre application précédente, nous allons voir certaines techniques qui vous permettront de gérer les éventuels débordements imputés aux threads.
Il y a une fuite
Une erreur que nous avons commise est d'utiliser le handler en classe interne. Le problème de cette démarche est que quand on déclare une classe interne, alors chaque instance de cette classe contient une référence à la classe externe. Par conséquent, tant qu'il y a des messages sur la pile des messages qui sont liés au handler, l'activité ne pourra pas être nettoyée par le système, et une activité, ça pèse lourd pour le système !
Une solution simple est de faire une classe externe qui dérive de Handler
, et de rajouter une instance de cette classe en tant qu'attribut.
1 2 3 4 5 6 7 8 9 10 11 12 | import android.app.ProgressDialog; import android.os.Handler; import android.os.Message; public class ProgressHandler extends Handler { @Override public void handleMessage(Message msg) { super.handleMessage(msg); ProgressDialog progressBar = (ProgressDialog)msg.obj; progressBar.setProgress(msg.arg1); } } |
Ne pas oublier d'inclure la boîte de dialogue dans le message puisque nous ne sommes plus dans la même classe ! Si vous vouliez vraiment rester dans la même classe, alors vous auriez pu déclarer ProgressHandler
comme statique de manière à séparer les deux classes.
Gérer le cycle de l'activité
Il faut lier les threads au cycle des activités. On pourrait se dire qu'on veut parfois effectuer des tâches d'arrière-plan même quand l'activité est terminée, mais dans ce cas-là on ne passera pas par des threads mais par des Services
, qui seront étudiés dans le prochain chapitre.
Le plus important est de gérer le changement de configuration. Pour cela, tout se fait dans onRetainNonConfigurationInstance()
. On fait en sorte de sauvegarder le thread ainsi que la boîte de dialogue de manière à pouvoir les récupérer :
1 2 3 4 5 6 | public Object onRetainNonConfigurationInstance () { List<Object> list = new ArrayList<Object>(); list.add(mProgressBar); list.add(mProgress); return list; } |
Enfin, vous pouvez aussi faire en sorte d'arrêter le thread dès que l'activité passe en pause ou quitte son cycle.
AsyncTask
Il faut avouer que tout cela est bien compliqué et nécessite de penser à tout, ce qui est source de confusion. Je vais donc vous présenter une alternative plus évidente à maîtriser, mais qui est encore une fois réservée à l’interaction avec le thread UI. AsyncTask
vous permet de faire proprement et facilement des opérations en parallèle du thread UI. Cette classe permet d'effectuer des opérations d'arrière-plan et de publier les résultats dans le thread UI sans avoir à manipuler de threads ou de handlers.
AsyncTask
n'est pas une alternative radicale à la manipulation des threads, juste un substitut qui permet le travail en arrière-plan sans toucher les blocs de bas niveau comme les threads. On va surtout les utiliser pour les opérations courtes (quelques secondes tout au plus) dont on connaît précisément l'heure de départ et de fin.
On ne va pas utiliser directement AsyncTask
, mais plutôt créer une classe qui en dérivera. Cependant, il ne s'agit pas d'un héritage évident puisqu'il faut préciser trois paramètres :
- Le paramètre
Params
permet de définir le typage des objets sur lesquels on va faire une opération. - Le deuxième paramètre,
Progress
, indique le typage des objets qui indiqueront l'avancement de l'opération. - Enfin,
Result
est utilisé pour symboliser le résultat de l'opération.
Ce qui donne dans le contexte :
1 | public class MaClasse extends AsyncTask<Params, Progress, Result> |
Ensuite, pour lancer un objet de type MaClasse
, il suffit d'utiliser dessus la méthode final AsyncTask<Params, Progress, Result> execute (Params... params)
sur laquelle il est possible de faire plusieurs remarques :
- Son prototype est accompagné du mot-clé
final
, ce qui signifie que la méthode ne peut être redéfinie dans les classes dérivées d'AsyncTask
. - Elle prend un paramètre de type
Params...
ce qui pourra en troubler plus d'un, j'imagine. Déjà,Params
est tout simplement le type que nous avons défini auparavant dans la déclaration deMaClasse
. Ensuite, les trois points signifient qu'il s'agit d'arguments variables et que par conséquent on peut en mettre autant qu'on veut. Si on prend l'exemple de la méthodeint somme(int... nombres)
, on peut l'appeler avecsomme(1)
ousomme(1,5,-2)
. Pour être précis, il s'agit en fait d'un tableau déguisé, vous pouvez donc considérernombres
comme unint[]
.
Une fois cette méthode exécutée, notre classe va lancer quatre méthodes de callback, dans cet ordre :
void onPreExecute()
est lancée dès le début de l'exécution, avant même que le travail commence. On l'utilise donc pour initialiser les différents éléments qui doivent être initialisés.- Ensuite, on trouve
Result doInBackground(Params... params)
, c'est dans cette méthode que doit être effectué le travail d'arrière-plan. À la fin, on renvoie le résultat de l'opération et ce résultat sera transmis à la méthode suivante — on utilise souvent unboolean
pour signaler la réussite ou l'échec de l'opération. Si vous voulez publier une progression pendant l'exécution de cette méthode, vous pouvez le faire en appelantfinal void publishProgress(Progress... values)
(la méthode de callback associée étantvoid onProgressUpdate(Progress... values)
). - Enfin,
void onPostExecute(Result result)
permet de conclure l'utilisation de l'AsyncTask
en fonction du résultatresult
passé en paramètre.
De plus, il est possible d'annuler l'action d'un AsyncTask
avec final boolean cancel(boolean mayInterruptIfRunning)
, où mayInterruptIfRunning
vaut true
si vous autorisez l'exécution à s'interrompre. Par la suite, une méthode de callback est appelée pour que vous puissez réagir à cet évènement : void onCancelled()
.
Enfin, dernière chose à savoir, un AsyncTask
n'est disponible que pour une unique utilisation, s'il s'arrête ou si l'utilisateur l'annule, alors il faut en recréer un nouveau.
Et cette fois on fait comment pour gérer les changements de configuration ?
Ah ! vous aimez avoir mal, j'aime ça. Accrochez-vous parce que ce n'est pas simple. Ce que nous allons voir est assez avancé et de bas niveau, alors essayez de bien comprendre pour ne pas faire de boulettes quand vous l'utiliserez par la suite.
On pourrait garder l'activité qui a lancé l'AsyncTask
en paramètre, mais de manière générale il ne faut jamais garder de référence à une classe qui dérive de Context
, par exemple Activity
. Le problème, c'est qu'on est bien obligés par moment. Alors comment faire ?
Revenons aux bases de la programmation. Quand on crée un objet, on réserve dans la mémoire allouée par le processus un emplacement qui aura la place nécessaire pour mettre l'objet. Pour accéder à l'objet, on utilise une référence sous forme d'un identifiant déclaré dans le code :
1 | String chaine = new String(); |
Ici, chaine
est l'identifiant, autrement dit une référence qui pointe vers l'emplacement mémoire réservé pour cette chaîne de caractères.
Bien sûr, au fur et à mesure que le programme s'exécute, on va allouer de la place pour d'autres objets et, si on ne fait rien, la mémoire va être saturée. Afin de faire en sorte de libérer de la mémoire, un processus qui s'appelle le garbage collector (« ramasse-miettes » en français) va détruire les objets qui ne sont plus susceptibles d'être utilisés :
1 2 3 4 5 | String chaine = new String("Rien du tout"); if(chaine.equals("Quelque chose") { int dix = 10; } |
La variable chaine
sera disponible avant, pendant et après le if
puisqu'elle a été déclarée avant (donc de 1 à 5, voire plus loin encore), en revanche dix
a été déclaré dans le if
, il ne sera donc disponible que dedans (donc de 4 à 5). Dès qu'on sort du if
, le garbage collector passe et désalloue la place réservée dix
de manière à pouvoir l'allouer à un autre objet.
Quand on crée un objet en Java, il s'agit toujours d'une référence forte, c'est-à-dire que l'objet est protégé contre le garbage collector tant qu'on est certain que vous l'utilisez encore. Ainsi, si on garde notre activité en référence forte en tant qu'attribut de classe, elle restera toujours disponible, et vous avez bien compris que ce n'était pas une bonne idée, surtout qu'une référence à une activité est bien lourde.
À l'opposé des références fortes se trouvent les références faibles. Les références faibles ne protègent pas une référence du garbage collector.
Ainsi, si vous avez une référence forte vers un objet, le garbage collector ne passera pas dessus. Si vous en avez deux, idem. Si vous avez deux références fortes et une référence faible, c'est la même chose, parce qu'il y a deux références fortes.
Si le garbage collector réalise que l'une des deux références fortes n'est plus valide, l'objet est toujours conservé en mémoire puisqu'il reste une référence forte. En revanche, dès que la seconde référence forte est invalidée, alors l'espace mémoire est libéré puisqu'il ne reste plus aucune référence forte, juste une petite référence faible qui ne protège pas du ramasse-miettes.
Ainsi, il suffit d'inclure une référence faible vers notre activité dans l'AsyncTask
pour pouvoir garder une référence vers l'activité sans pour autant la protéger contre le ramasse-miettes.
Pour créer une référence faible d'un objet T
, on utilise WeakReference
de cette manière :
1 2 | T strongReference = new T(); WeakReference<T> weakReference = new WeakReference<T>(strongReference); |
Il n'est bien entendu pas possible d'utiliser directement un WeakReference
, comme il ne s'agit que d'une référence faible, il vous faut donc récupérer une référence forte de cet objet. Pour ce faire, il suffit d'utiliser T get()
. Cependant, cette méthode renverra null
si l'objet a été nettoyé par le garbage collector.
Application
Énoncé
Faites exactement comme l'application précédente, mais avec un AsyncTask
cette fois.
Spécifications techniques
L'AsyncTask
est utilisé en tant que classe interne statique, de manière à ne pas avoir de fuites comme expliqué dans la partie consacrée aux threads.
Comme un AsyncTask
n'est disponible qu'une fois, on va en recréer un à chaque fois que l'utilisateur appuie sur le bouton.
Il faut lier une référence faible à votre activité à l'AsyncTask
pour qu'à chaque fois que l'activité est détruite on reconstruise une nouvelle référence faible à l'activité dans l'AsyncTask
. Un bon endroit pour faire cela est dans le onRetainNonConfigurationInstance()
.
Ma solution
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 | package sdz.chapitreQuatre.async.example; import java.lang.ref.WeakReference; import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.DialogInterface; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; public class AsyncActivity extends Activity { // Taille maximale du téléchargement public final static int MAX_SIZE = 100; // Identifiant de la boîte de dialogue public final static int ID_DIALOG = 0; // Bouton qui permet de démarrer le téléchargement private Button mBouton = null; private ProgressTask mProgress = null; // La boîte en elle-même private ProgressDialog mDialog = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBouton = (Button) findViewById(R.id.bouton); mBouton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { // On recrée à chaque fois l'objet mProgress = new ProgressTask(AsyncActivity.this); // On l'exécute mProgress.execute(); } }); // On recupère l'AsyncTask perdu dans le changement de configuration mProgress = (ProgressTask) getLastNonConfigurationInstance(); if(mProgress != null) // On lie l'activité à l'AsyncTask mProgress.link(this); } @Override protected Dialog onCreateDialog (int id) { mDialog = new ProgressDialog(this); mDialog.setCancelable(true); mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface arg0) { mProgress.cancel(true); } }); mDialog.setTitle("Téléchargement en cours"); mDialog.setMessage("C'est long..."); mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); mDialog.setMax(MAX_SIZE); return mDialog; } @Override public Object onRetainNonConfigurationInstance () { return mProgress; } // Met à jour l'avancement dans la boîte de dialogue void updateProgress(int progress) { mDialog.setProgress(progress); } // L'AsyncTask est bien une classe interne statique static class ProgressTask extends AsyncTask<Void, Integer, Boolean> { // Référence faible à l'activité private WeakReference<AsyncActivity> mActivity = null; // Progression du téléchargement private int mProgression = 0; public ProgressTask (AsyncActivity pActivity) { link(pActivity); } @Override protected void onPreExecute () { // Au lancement, on affiche la boîte de dialogue if(mActivity.get() != null) mActivity.get().showDialog(ID_DIALOG); } @Override protected void onPostExecute (Boolean result) { if (mActivity.get() != null) { if(result) Toast.makeText(mActivity.get(), "Téléchargement terminé", Toast.LENGTH_SHORT).show(); else Toast.makeText(mActivity.get(), "Echec du téléchargement", Toast.LENGTH_SHORT).show(); } } @Override protected Boolean doInBackground (Void... arg0) { try { while(download() != MAX_SIZE) { publishProgress(mProgression); Thread.sleep(1000); } return true; }catch(InterruptedException e) { e.printStackTrace(); return false; } } @Override protected void onProgressUpdate (Integer... prog) { // À chaque avancement du téléchargement, on met à jour la boîte de dialogue if (mActivity.get() != null) mActivity.get().updateProgress(prog[0]); } @Override protected void onCancelled () { if(mActivity.get() != null) Toast.makeText(mActivity.get(), "Annulation du téléchargement", Toast.LENGTH_SHORT).show(); } public void link (AsyncActivity pActivity) { mActivity = new WeakReference<AsyncActivity>(pActivity); } public int download() { if(mProgression <= MAX_SIZE) { mProgression++; return mProgression; } return MAX_SIZE; } } } |
Pour terminer, voici une liste de quelques comportements à adopter afin d'éviter les aléas des blocages :
- Si votre application fait en arrière-plan de gros travaux bloquants pour l'interface graphique (imaginez qu'elle doive télécharger une image pour l'afficher à l'utilisateur), alors il suffit de montrer l'avancement de ce travail avec une barre de progression de manière à faire patienter l'utilisateur.
- En ce qui concerne les jeux, usez et abusez des threads pour effectuer des calculs de position, de collision, etc.
- Si votre application a besoin de faire des initialisations au démarrage et que par conséquent elle met du temps à se charger, montrez un splash screen avec une barre de progression pour montrer à l'utilisateur que votre application n'est pas bloquée.
- Par défaut, au lancement d'une application, le système l'attache à un nouveau processus dans lequel il sera exécuté. Ce processus contiendra tout un tas d'informations relatives à l'état courant de l'application qu'il contient et des threads qui exécutent le code.
- Vous pouvez décider de forcer l'exécution de certains composants dans un processus à part grâce à l'attribut
android:process
à rajouter dans l'un des éléments constituant le noeud<application>
de votre manifest. - Lorsque vous jouez avec les threads, vous ne devez jamais perdre à l'esprit deux choses :
- Ne jamais bloquer le thread UI.
- Ne pas manipuler les vues standards en dehors du thread UI.
- On préfèrera toujours privilégier les concepts de haut niveau pour faciliter les manipulations pour l'humain et ainsi donner un niveau d'abstraction aux contraintes machines. Pour les threads, vous pouvez donc privilégier les messages et les handlers à l'utilisation directe de la classe
Thread
. AsyncTask
est un niveau d'abstraction encore supérieur aux messages et handlers. Elle permet d'effectuer des opérations en arrière-plan et de publier les résultats dans le thread UI facilement grâce aux méthodes qu'elle met à disposition des développeurs lorsque vous en créez une.