• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 1/13/20

Programmez des tâches asynchrones

Log in or subscribe for free to enjoy all this course has to offer!

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 :

  1. Règle 1: Do not block the UI thread

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

Example of certificate of achievement
Example of certificate of achievement