L'UI thread
Date de publication : 7 avril 2010
Par
Davy Leggieri (Page de Davy Leggieri)
Cet article présente l'UI thread et les principes de base pour interagir convenablement avec celui-ci.
I. Introduction
II. Le fonctionnement de l'UI thread
III. Le responsable de l'affichage et des interactions avec l'utilisateur
IV. Un thread à préserver
V. Où mettre les instructions consommatrices en temps ?
VI. Ordonner des tâches à l'UI thread
VI-A. Ordonner depuis l'activity
VI-B. Ordonner depuis une view
VI-C. Ordonner grâce à un handler
VI-D. Ordonner grâce à une AsyncTask
VII. Conclusion
VIII. Remerciements
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 coeur 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 mono-thread 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) {
super.onCreate(savedInstanceState);
...
unButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
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 {
Thread.sleep(3000L);
} catch (InterruptedException e) {}
}
});
}
...
}
|
Avant le clic sur le bouton, nous ne constatons rien de particulier.

État du bouton avant le clic
Après le clic sur le bouton, nous observons que ce dernier conserve l'état "appuyé".
Ce comportement dure 3 secondes, ce qui correspond au temps de pause de l'UI thread.

État du bouton pendant le clic et durant 3 secondes
Pour comprendre ce qui se passe, je propose que nous déroulions le code :
L'UI Thread :
-
...
-
(Clic sur le bouton "unButton")
-
x è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 3 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) {
super.onCreate(savedInstanceState);
...
unButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
}
}).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) {
super.onCreate(savedInstanceState);
...
unButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
UpdateIHM(resultat);
}
}).start();
}
}
public void UpdateIHM(String resultat)
{
runOnUiThread(new Runnable() {
@Override
public void run() {
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).
|
public class monButton extends Button {
public monButton(Context context) {
super(context);
}
...
public boolean onKeyDown(int keyCode, KeyEvent event){
new Thread(new Runnable() {
@Override
public void run() {
updateIHM(resultat);
}
}).start();
return super.onKeyDown(keyCode, event);
}
private void updateIHM(String resultat){
post(new Runnable() {
@Override
public void run() {
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.
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");
}
}
|
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


Copyright © 2010 Davy Leggieri. 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.
Cette page est déposée.