Thread principal responsable de l’IHM
Dans cette section, on va s’intéresser au rôle particulier du thread principal. On évoquera aussi les problèmes qui sont soulevés lorsqu’on utilise des threads secondaires. Ces problèmes pourraient être évités si on n'utilisait jamais de thread secondaire, mais malheureusement, nous allons voir qu’ils sont nécessaires pour le traitement des tâches longues.
Afin de minimiser la consommation de ressources matérielles, un seul thread principal est responsable de la totalité de l’exécution des différentes tâches de l’application. Il faut en tout premier lieu, rafraîchir l’interface graphique et donc exécuter très régulièrement cette tâche. Il faut gérer les messages de l’application, c’est-à-dire les Intent
en entrée et en sortie. Si des événements surviennent à cause d’interactions utilisateur, c’est le thread principal qui va exécuter les callbacks associés à l'événement (même les services sont exécutés par ce thread). À cause de son importance, deux règles sont données dans la documentation :
Règle 1: Do not block the UI thread
Règle 2: Do not access the Android UI toolkit from outside the UI thread
Cette dernière règle est particulièrement importante. En effet, une mauvaise programmation peut générer des conflits d’accès à l’interface graphique. Si deux threads accèdent au même éléments graphiques, on ne sait pas dans quel ordre ils pourront agir sur cet élément. Pire encore, si un des thread supprime un élément graphique, l’autre thread risque d’avoir un pointeur null
, et éventuellement lever une exception.
L'exemple suivant montre une violation de la deuxième règle, et donc une mauvaise programmation. En effet, un thread t
agit sur l'interface graphique, en retirant le TextView tv
du gabarit l
(lignes 13-17). À cause de l'ajout d'une attente (ligne 21), le code des lignes 24-25 va s'exécuter après le code du thread t
et le TextView
n'existera donc plus.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Thread t = new Thread() {
@Override
public void run() {
TextView tv = (TextView)findViewById(R.id.hello);
RelativeLayout l =
(RelativeLayout) findViewById(R.id.rootlayout);
l.removeView(tv);
}
};
t.start();
try { Thread.sleep(1000); }
catch (InterruptedException e) { e.printStackTrace(); }
TextView tv = (TextView)findViewById(R.id.hello);
tv.setText("Changing text of the textview (if still exists !)");
}
}
Deux solutions permettent de résoudre ce problème. On pourrait utiliser un mécanisme d’exclusion mutuelle pour décider des accès à l’interface graphique. Des classes Java pour gérer les verrous et blocs synchronisés existent dans le langage, mais ce n’est pas le choix que Google a effectué. Pour simplifier la programmation et éviter les bugs liés à la difficulté de la bonne gestion de l’exclusion mutuelle, Google a préféré définir une convention :seul le thread principal modifie l’interface graphique.
Ainsi, seul le thread principal manipule les éléments de l’interface graphique. Si un thread secondaire souhaite modifier l’interface graphique, l’API prévoit des méthodes permettant d’envoyer du code pour un traitement ultérieur par le thread principal. On peut appeler ces méthodes depuis l’activité grâce àrunOnUiThread(Runnable)
ou bien depuis un élément graphique qui hérite de View
viaView.post(Runnable)
et View.postDelayed(Runnable, long)
.
On pourrait se dire qu'il est plus facile de ne jamais utiliser de Thread
pour éviter tout problème. Seulement, dans ce cas, certaines tâches longues ne pourraient jamais être exécutées car elles bloqueraient le thread principal, ce qui est interdit (cela bloquerait l'interface graphique de l'application). C'est donc pour cela qu'il est nécessaire de déporter des tâches longues dans des threads secondaires.
Thread secondaire dans une AsyncTask
Dans cette section, on s’intéresse à la gestion des tâches longues. On pourrait utiliser de simples threads, comme vu précédemment, mais il existe la classe spécifique AsyncTask
qui apporte de nombreux avantages.
Pendant l’exécution d’une activité, il est possible qu’une tâche longue survienne (calcul, attente d’un périphérique). Dans ce cas, cette tâche interrompt l’exécution de l’application, et notamment l’interface graphique, ce qui est problématique. On pourrait utiliser un thread, comme vu précédemment, mais il existe une classe spécifique à Android plus simple à utiliser : AsyncTask
.

AsyncTask est une classe générique utilisant trois token U, le type de l’input, V, le type de l’objet de notification (s’il y en a une), et W le type du résultat. On démarre la tâche en appelant la méthode execute(U)
et s'enchaîne alors trois méthodes de la classe :
onPreExecute()
est appelée et permet de préparer la tâche, par exemple l’interface graphique (animation d’attente, notification).W doInBackground(U)
est ensuite appelée et s’exécute dans un thread secondaire. Elle reçoit le paramètre d’input de la tâche et à la fin, retourne un résultat de type W.onPostExecute(W)
est enfin appelée, et s’exécute dans le thread principal : elle permet d’afficher le résultat par exemple.onProgressUpdate(V...)
permet de gérer les notifications de progression de la tâche.
Dans le cas ou ces méthodes ne prennent aucun paramètres, on peut utiliser le type Void
qui est un type spécifique permettant de passer un pointeur null
à la place d'un paramètre typé. Par exemple, si l'on démarre une tâche sans paramètre, on fera : a.execute((Void)null)
.
Premier exemple d'AsyncTask : production en fin de tâche
Du point de vue du code, un exemple est donné ci-dessous. Dans la tâche, on calcule le nième terme de la suite de Fibonacci avec l'appel statique Fibonacci.fibo(integers[0]);
. À la fin dedoInBackground(integers);
, le nième terme de la suite a été calculé et on le retourne comme résultat. Tout le code de doInBackground()
est exécuté dans un thread secondaire. Par contre, la méthode onPostExecute(Integer res)
est exécutée par le thread principal. Cette méthode peut donc, sans violer la règle 1 (Do not access the Android UI toolkit from outside the UI thread) modifier l'interface graphique pour afficher le résultat dans le TextView
resultTV.
public class FiboTask extends AsyncTask<Integer, Void, Integer> {
private AppCompatActivity myActivity;
public FiboTask(AppCompatActivity a) {
myActivity = a;
}
@Override
protected Integer doInBackground(Integer... integers) {
int result = Fibonacci.fibo(integers[0]); // Can take a long time!
return result;
}
@Override
protected void onPostExecute(Integer res) {
ProgressBar pb = (ProgressBar)myActivity.findViewById(R.id.progressBar);
pb.setVisibility(View.GONE);
TextView resultTV = (TextView)myActivity.findViewById(R.id.result);
resultTV.setText("" + res);
}
}
Second exemple d'AsyncTask : production pendant la tâche
Il est également possible d'utiliser une AsyncTask
pour produire un résultat en récupérant les données au fur et à mesure. Par exemple on pourrait imaginer une tâche ayant pour but de faire une requête HTTP sur une URL (qui serait le paramètre d'entrée) et qui publierait chacun des caractères reçus à l'écran. C'est inutile et lent, mais voici comment on pourrait l'écrire. On notera les type String
et Character
utilisé pour le type de paramètre d'entrée et pour le type de production intermédiaire. La signature de doInBackground
est écrite en conséquence, ainsi que celle de onProgressUpdate
.
public class HTTPTask extends AsyncTask<String, Character, Void> {
private MainActivity activity;
public HTTPTask(MainActivity activity){
this.activity=activity;
}
@Override
protected Void doInBackground(String... url) {
String request = "GET / HTTP/1.1\nHost: "+url[0]+"\n\n";
Socket sock = null;
try {
sock = new Socket(url[0],80);
PrintWriter out = new PrintWriter(sock.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
// Envoie de l'entête HTTP Get
out.print(request);
out.flush();
// Lecture caractère par caractère
boolean doLoop = true;
while(doLoop) {
int c = in.read();
if (c!=-1) {
publishProgress(new Character((char)c));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onProgressUpdate(Character... values) {
super.onProgressUpdate(values);
StringBuilder s = new StringBuilder();
for (Character c:values) {
s.append(c);
}
activity.displayCharacter(s.toString());
}
}
Pour lancer l'exécution de cet code, il faudra créer une instance de la classe, puis lancer son exécution en fournissant l'URL à interroger :
HTTPTask tache = new HTTPTask(MainActivity.this);
tache.execute(urlTV.getText().toString());
Ainsi, avec la classe AsyncTask, nous sommes capables d’implémenter des tâches longues, par exemple des tâches réseaux, sans interrompre le bon fonctionnement de mon activité. C'est ce qui est abordé dans le chapitre suivant.