L'UI thread

Cet article présente l'UI thread et les principes de base pour interagir convenablement avec celui-ci.

15 commentaires Donner une note à l'article (4.5)

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

 
Sélectionnez
...
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é.

 
Sélectionnez

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.

 
Sélectionnez
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(3000L);
                } catch (InterruptedException e) {}
            }
        });
    }
    ...
}

Avant le clic sur le bouton, nous ne constatons rien de particulier.

Image non disponible
É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.

Image non disponible
É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.

 
Sélectionnez
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.

 
Sélectionnez
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).

 
Sélectionnez

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

 
Sélectionnez
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()".

 
Sélectionnez
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.


Android :
Création d'un carrousel
Création d'une boussole
Les threads composants une application
L'UI thread
  

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.