I. Introduction▲
Le thread principal, appelé plus communément User Interface thread ou UI thread, est le point d'orgue de toute application Android. En effet, ce dernier remplit plusieurs rôles. Tout d'abord, il exécute le code de l'Activity ou du Service. Ensuite, dans le cas d'une Activity, il intercepte les interactions de l'utilisateur. Et pour finir, il est responsable de l'affichage de l'Activity. Toutes ces actions pour un unique thread demandent de la rigueur à l'utilisation.
L'intention de l'article est de présenter l'UI thread et les différentes manières de l'utiliser convenablement.
II. Le fonctionnement de l'UI thread▲
L'UI thread est un thread dédié aux traitements des interactions utilisateurs et à l'affichage. Ainsi lorsque vous touchez du doigt l'écran du dispositif Android et qu'une action s'en suit, c'est grâce à ce thread. De la même manière lorsqu'on constate un changement de l'affichage tel que le texte d'un bouton qui est modifié, c'est encore grâce à l'UI thread. On ne peut que constater que ce thread est au cœur de toute application Java sous Android.
Intéressons-nous à présent au fonctionnement de ce thread. Pour réaliser ses différents rôles, l'UI thread est implémenté sous la forme d'une boucle infinie. Cette dernière va lui permettre de continuellement scruter s'il y a des « choses à faire » telles qu'une modification de l'affichage ou l'exécution du code correspondant à la pression d'un bouton précis.
...
tant que ( vrai )
{
si (il y a au moins une chose à faire) alors
{
faire la chose
//Ex1: dessiner à l'écran
//EX2: exécuter le code lié au bouton cliqué
}
}
...
Avant de rentrer dans la boucle infinie, ce thread exécute les méthodes onCreate(), onStart(), onResume(). Et à l'inverse, lorsque l'Activity se ferme, il sort de la boucle infinie pour ensuite exécuter les méthodes onPause(), onStop(), onDestroy().
III. Le responsable de l'affichage et des interactions avec l'utilisateur▲
Android est basé sur un fonctionnement monothread pour accomplir les opérations dont est responsable l'UI Thread. À l'usage ceci est visible notamment lorsque nous utilisons des éléments graphiques qui doivent être impérativement exécutés par l'UI thread. En effet, par exemple, l'appel à la méthode unTextView.setText(« Nouveau text ») n'est pas thread safe, et il en est de même pour toutes les modifications de l'affichage. « Thread safe » signifie simplement que l'action ne peut être réalisée de manière déterministe depuis n'importe quel thread. En conclusion, l'UI thread doit être le seul thread à effectuer des modifications sur l'affichage.
Ce choix de conception peut trouver une explication assez simple. Nos téléphones disposant d'un seul écran, il apparaît logique qu'un seul thread soit responsable de l'affichage. Ce thread est l'unique voie de communication avec l'affichage pour tous les autres threads. L'avantage étant qu'en centralisant l'information dans un seul thread, nous nous assurons de la cohérence de ce qui est affiché.
public
class
TestActivity extends
Activity {
...
public
void
onCreate
(
Bundle savedInstanceState) {
//Code exécuté dans l'UI thread
super
.onCreate
(
savedInstanceState);
...
//On lie une action au bouton
unButton.setOnClickListener
(
new
OnClickListener
(
) {
//Définition de l'action qui sera exécutée par l'UI thread lors d'un clic
@Override
public
void
onClick
(
View v) {
//Le code s'exécutant dans l'UI Thread, nous pouvons donc modifier l'IHM sans craintes
unTextView.setText
(
"Nouveau text"
);
}
}
}
...
}
Le code se déroule dans l'ordre chronologique suivant :
UI Thread :
- appel de onCreate(savedInstanceState) ;
- -- appel de super.onCreate(savedInstanceState) ;
- -- liaison d'une action au clic sur le bouton « unButton » ;
- appel de onStart() ;
- lors d'un clic sur le bouton « unButton », exécution l'action onClic(v) ;
- -- exécution de unTextView.setText(« Nouveau text »).
Pour le moment on peut constater que tout se déroule dans l'UI thread. L'enchaînement des actions est alors trivial, et nous respectons bien la règle que nous nous sommes fixée : modifier l'affichage uniquement depuis l'UI thread.
IV. Un thread à préserver▲
Reconsidérons l'algorithme de l'UI thread présenté précédemment. La clef de voûte de ce dernier était la boucle infinie par laquelle il était informé d'interactions de l'utilisateur ou alors d'ordres de modifications de l'affichage envoyés par d'autres threads. Si nous surchargeons le thread d'opérations consommatrices en temps CPU, nous allons bloquer temporairement l'UI thread qui ne bouclera pas. Le thread, ainsi occupé, ne pourra plus être dédié à sa tâche principale qui est la consultation d'interactions de l'utilisateur ou alors d'ordres de modifications de l'affichage.
Il faut préserver l'UI thread de tout traitement lourd sous peine de perturber l'affichage ou les interactions avec l'utilisateur. Pour nous en convaincre, nous allons mettre au point un exemple d'opération consommatrice en temps qui va être exécutée dans l'UI thread et le bloquer pendant un certain temps.
Pour que l'UI thread consomme du temps CPU, nous allons utiliser l'appel système Thread.sleep(temps) qui permet de mettre en pause un thread pendant x millisecondes. Afin de nous assurer que l'UI thread sera bien le thread impacté par cette instruction, nous allons placer celle-ci dans une portion de code effectivement exécutée par l'UI thread. Nous choisirons une méthode exécutée suite à une interaction de l'utitilisateur, ici un clic sur un bouton.
public
class
blockUIThread extends
Activity {
private
Button mButtonBlock;
...
@Override
public
void
onCreate
(
Bundle savedInstanceState) {
super
.onCreate
(
savedInstanceState);
setContentView
(
R.layout.main);
mButtonBlock =
(
Button) findViewById
(
R.id.ButtonBlock);
mButtonBlock.setOnClickListener
(
new
OnClickListener
(
) {
@Override
public
void
onClick
(
View v) {
try
{
//Stopper l'UI thread pendant 3 secondes
Thread.sleep
(
3000
L);
}
catch
(
InterruptedException e) {}
}
}
);
}
...
}
Avant le clic sur le bouton, nous ne constatons rien de particulier.
Après le clic sur le bouton, nous observons que ce dernier conserve l'état « appuyé ». Ce comportement dure trois secondes, ce qui correspond au temps de pause de l'UI thread.
Pour comprendre ce qui se passe, je propose que nous déroulions le code :
L'UI Thread :
- …
- (Clic sur le bouton « unButton »)
- xième passage dans la boucle infinie
- -- Est-ce qu'il y a au moins une chose à faire ?
- ---- Il y a le clic sur le bouton à traiter
- ------- Demander au bouton de se dessiner enfoncé
- -------- Exécuter le code lié au clic sur le bouton
- ---------- Pause du thread pendant trois secondes
- -------- Demander au bouton de se dessiner relâché (état normal)
- x+1 ème passage dans la boucle infinie
- …
Nous comprenons très bien que le traitement de l'UI thread va retarder toutes les autres tâches qu'il est censé faire. Nous devons donc proscrire les actions consommatrices en temps de l'UI thread.
V. Où mettre les instructions consommatrices en temps ?▲
L'UI thread ne pouvant exécuter ce type d'actions, nous allons tout simplement créer un thread qui va les réaliser. Pour cela, nous disposons de la classe Thread qui, comme son nom le suggère, permet de créer un thread.
Chaque thread possédant son propre fil d'exécution, nous allons avoir des instructions qui vont être exécutées en parallèle. Si vous désirez plus d'informations sur les threads Java, je vous invite à consulter le tutoriel suivant : « Programmation des Threads en Java ».
En général, nous instancions l'objet Thread avec un Runnable qui n'est que le moyen de stocker le code qui sera exécuté par le thread. Une fois l'objet ainsi instancié, le thread n'est pas à proprement parler créé. Nous devons exécuter la méthode start() pour créer un nouveau fil d'exécution et lancer l'exécution du thread. Sa durée de vie s'étale de l'appel à cette méthode jusqu'à ce que toutes les instructions du Runnable soient exécutées.
public
class
TestActivity extends
Activity {
...
public
void
onCreate
(
Bundle savedInstanceState) {
//Code executé dans le thread principal
super
.onCreate
(
savedInstanceState);
...
//On lie une action au bouton
unButton.setOnClickListener
(
new
OnClickListener
(
) {
@Override
public
void
onClick
(
View v) {
//Création et exécution d'un nouveau thread
new
Thread
(
new
Runnable
(
) {
@Override
public
void
run
(
) {
// Instructions consommatrices en temps
}
}
).start
(
);
}
}
...
}
Je propose que nous déroulions le code :
UI Thread :
- appel de onCreate(savedInstanceState) ;
- -- appel de super.onCreate(savedInstanceState) ;
- -- liaison d'une action au clic sur le bouton unButton ;
- …
- (clic sur le bouton « unButton ») ;
- n-ième passage dans la boucle infinie ;
- -- est-ce qu'il y a au moins une chose à faire ? ;
- ---- il y a le clic sur le bouton à traiter ;
- ------- demander au bouton de se dessiner enfoncé ;
- -------- exécuter le code lié au clic sur le bouton ;
- ---------- création et exécution du nouveau thread ;
- -------- demander au bouton de se dessiner relâché (état normal) ;
- n-ième+1 passage dans la boucle infinie.
- …
Le nouveau thread :
- Exécution de la méthode run() ;
- -- exécution de l'opération consommatrice en temps ;
- mort du thread.
Avec la création d'un nouveau thread, l'UI thread se retrouve délesté et n'est plus bloqué. De cette manière, après le clic sur le bouton, l'UI thread crée un nouveau thread et peut rapidement retourner à sa tâche principale qui est de boucler perpétuellement.
VI. Ordonner des tâches à l'UI thread▲
L'UI thread est le seul thread pouvant modifier l'affichage. Or, dans le cas précédant avec la création d'un nouveau thread, ce dernier ne possède pas le droit de modifier l'affichage. Pour résoudre ce problème, il ne faut pas oublier que l'UI thread est la seule voie de communication sûre pour modifier l'affichage. Nous allons donc présenter comment communiquer avec l'UI thread pour lui ordonner des actions sur l'affichage.
VI-A. Ordonner depuis l'activity▲
Toute portion de code rédigée dans la classe Activity possède une aisance pour communiquer avec l'UI thread. En effet, la méthode runOnUIThread() qui accepte un Runnable en paramètre permet de déposer une tâche dans la boucle infinie de l'UI thread. Ainsi, nous pourrons envoyer des ordres pour modifier l'affichage.
public
class
TestActivity extends
Activity {
...
public
void
onCreate
(
Bundle savedInstanceState) {
//Code executé dans le thread principal
super
.onCreate
(
savedInstanceState);
...
//On lie une action au bouton
unButton.setOnClickListener
(
new
OnClickListener
(
) {
@Override
public
void
onClick
(
View v) {
//Création et exécution d'un nouveau thread
new
Thread
(
new
Runnable
(
) {
@Override
public
void
run
(
) {
//Opération consommatrice en temps
//Appeler d'une méthode (pour améliorer la lisibilité du code)
UpdateIHM
(
resultat);
}
}
).start
(
);
}
}
public
void
UpdateIHM
(
String resultat)
{
//Déposer le Runnable dans la file d'attente de l'UI thread
runOnUiThread
(
new
Runnable
(
) {
@Override
public
void
run
(
) {
//code exécuté par l'UI thread
mTextView.setText
(
resultat);
}
}
);
}
...
}
Comme à l'accoutumée, je propose que nous déroulions le code :
L'UI Thread :
- appel de onCreate(savedInstanceState) ;
- -- appel de super.onCreate(savedInstanceState) ;
- -- liaison d'une action au clic sur le bouton unButton ;
- …
- (clic sur le bouton « unButton ») ;
- n-ième passage dans la boucle infinie ;
- -- est-ce qu'il y a au moins une chose à faire ? ;
- ---- il y a le clic sur le bouton à traiter ;
- -------- exécuter le code lié au clic sur le bouton ;
- ---------- création et exécution du nouveau thread ;
- n-ième passage dans la boucle infinie.
- …
Le nouveau thread :
- Exécution de la méthode run() ;
- -- exécution de UpdateIHM(resultat) ;
- ---- exécution de runOnUiThread(…) ;
- ------ déposer le Runnable dans la file d'attente de l'UI thread ;
- mort du thread.
L'UI Thread (suite) :
- …
- n-ième passage dans la boucle infinie ;
- -- est-ce qu'il y a au moins une chose à faire ? ;
- ---- il y a le Runnable à exécuter ;
- -------- exécuter « mTextView.setText(…) » ;
- n-ième+1 passage dans la boucle infinie.
- …
VI-B. Ordonner depuis une view▲
Depuis une classe enfant de View nous pouvons envoyer des Runnable à l'UI thread grâce à la méthode post(). Les classes descendantes de View sont les seules classes impactées par ce comportement, et elles correspondent à toutes les classes dédiées à l'affichage (les layouts, les boutons, les images, les textView, etc.).
Le recours à cette méthode pour ordonner des actions à l'UI thread est assez rare. En effet, les cas d'utilisations ne sont pas légende, et cela est principalement utilisé lorsque nous définissons nos classes de vue par héritage (création d'une boussole, création d'un textView ayant un comportement spécifique).
//Attention, nous ne sommes plus dans une Activity dans le cas présent
public
class
monButton extends
Button {
public
monButton
(
Context context) {
super
(
context);
//code exécuté par l'UI thread
}
...
public
boolean
onKeyDown
(
int
keyCode, KeyEvent event){
//Création du thread par l'UI thread
new
Thread
(
new
Runnable
(
) {
@Override
public
void
run
(
) {
// Opération consommatrice en temps exécuté par le nouveau thread
//appel de updateIHM par le nouveau thread
updateIHM
(
resultat);
}
}
).start
(
);
return
super
.onKeyDown
(
keyCode, event); //Imposé par le SDK
}
private
void
updateIHM
(
String resultat){
//Déposer un tunnable dans l'UI thread
post
(
new
Runnable
(
) {
@Override
public
void
run
(
) {
//Code exécuté dans l'UI thread
setText
(
resultat);
}
}
);
}
...
}
Vous pouvez aussi utiliser la méthode postDelay(runnable,time) qui permet de déposer un Runnable dans la file d'attente de l'UI thread avec un certain retard.
Nous allons dérouler le code :
L'UI Thread :
- Instanciation du bouton ;
- …
- (pression d'une touche par l'utilisateur lorsque le focus est sur le bouton) ;
- n-ième passage dans la boucle infinie ;
- -- est-ce qu'il y a au moins une chose à faire ? ;
- ---- il y a la touche pressée à traiter ;
- ------ appel de la méthode 'onKeyDown()' ;
- -------- création et exécution du nouveau thread ;
- n-ième+1 passage dans la boucle infinie.
- …
Le nouveau thread :
- Exécution de la méthode run() ;
- -- exécution de UpdateIHM(resultat) ;
- ---- exécution de post(…) ;
- ------ déposer le Runnable dans la file d'attente de l'UI thread ;
- mort du thread.
L'UI Thread (suite) :
- …
- n-ième passage dans la boucle infinie ;
- -- est-ce qu'il y a au moins une chose à faire ? ;
- ---- il y a le Runnable à exécuter ;
- -------- exécuter « mTextView.setText(…) » ;
- n-ième+1 passage dans la boucle infinie.
- …
VI-C. Ordonner grâce à un handler▲
Nous pouvons également utiliser un handler pour soumettre des Runnable à l'UI thread. Le handler doit être instancié dans l'UI thread, et après cette action nous pouvons lui soumettre des Runnable depuis les autres threads grâce à la méthode post().
Nous ne détaillerons pas cette solution avec un exemple pour une seule et bonne raison : les méthodes pour soumettre des Runnable à l'UI thread présentées précédemment, runOnUiThread() pour l'activity ou post() pour une view, ont recours à des handlers dans leurs implémentations. Utiliser des Handlers reviendrait à réécrire ces méthodes. Puisque l'on met à notre disposition ces facilités, autant les utiliser.
Les handlers sont des files d'attente, ou encore ce que nous appelons jusqu'à présent boucle infinie (cf. boucle infinie dans l'algorithme de l'UI thread).
VI-D. Ordonner grâce à une AsyncTask▲
La classe AsyncTask permet de nous simplifier la vie lorsque nous désirons créer une tâche qui va faire un travail en arrière-plan et soumettre au fur et à mesure des choses à afficher. L'avantage de cette classe est qu'elle nous permet de nous abstraire des « threads » et des Runnable à déposer dans l'UI Thread.
Nous allons reprendre l'exemple d'utilisation des AsyncTask du site developer.android.com.
private
class
DownloadFilesTask extends
AsyncTask<
URL, Integer, Long>
{
//Action de la tâche asynchrone
protected
Long doInBackground
(
URL... urls) {
//Code exécuté dans un nouveau thread
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;
}
//Définition optionnelle
//Permet d'avertir l'utilisateur de la progression
protected
void
onProgressUpdate
(
Integer... progress) {
//Code exécuté dans l'UI thread pour chaque appel à publishProgress(...)
setProgressPercent
(
progress[0
]);
}
//Définition optionnelle
//Permet d'avertir l'utilisateur de la fin de la tâche asynchrone
protected
void
onPostExecute
(
Long result) {
//Code exécuté dans l'UI thread
showDialog
(
"Downloaded "
+
result +
" bytes"
);
}
}
Pour utiliser la classe ainsi déclarée, nous devons instancier un objet de ce type et appeler la méthode « execute() ».
new
DownloadFilesTask
(
).execute
(
url1, url2, url3);
Déroulons le code :
L'UI Thread :
- …
- Exécution de « new DownloadFilesTask().execute(…) » ;
- -- création d'un nouveau thread ;
- n-ième passage dans la boucle infinie.
- …
Le nouveau thread :
- Exécution de la méthode « doInBackground() » ;
- -- 1re exécution de « Downloader.downloadFile(..) » ;
- -- 1er appel de la méthode « publishProgress() ».
- …
L'UI Thread (suite) :
- …
- n-ième passage dans la boucle infinie ;
- -- est-ce qu'il y a au moins une chose à faire ? ;
- ---- il y a un Runnable à exécuter ;
- -------- exécuter « onProgressUpdate(…) » ;
- n-ième+1 passage dans la boucle infinie.
- …
Le nouveau thread (suite) :
- -- 2e exécution de « Downloader.downloadFile(..) » ;
- -- 2e appel de la méthode « publishProgress() » ;
- …
- -- ainsi de suite jusqu'à ce que l'opération effectuée par la tâche soit totalement terminée et qu'elle retourne « totalSize » ;
- mort du thread.
L'UI Thread (suite) :
- …
- n ème passage dans la boucle infinie
- -- Est-ce qu'il y a au moins une chose à faire ?
- ---- Il y a un Runnable à exécuter
- -------- Exécuter « onPostExecute(…) »
- n+1 ème passage dans la boucle infinie
- …
VII. Conclusion▲
L'UI thread d'une Activity regroupe plusieurs rôles. Il exécute les méthodes de l'activity puis, il « tombe » dans une boucle infinie qui lui permet de traiter les interactions de l'utilisateur et/ou de modifier l'affichage. Partant de ce constat, ce thread ne doit jamais être encombré par des traitements longs sous peine de dégrader l'expérience utilisateur.
J'espère que vous avez apprécié la lecture de cet article tout autant que j'en ai apprécié son écriture.
VIII. Remerciements▲
Je tiens à remercier les membres de l'équipe de Developpez.com, Baptiste Wicht et MrDuChnok pour leurs conseils et également Ellène, jacques_jean et Wachter pour leur relecture attentive.