Licence CC BY-NC-SA

Le travail en arrière-plan

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

L'un de vos objectifs prioritaires sera de travailler sur la réactivité de vos applications, c'est-à-dire de faire en sorte qu'elles ne semblent pas molles ou ne se bloquent pas sans raison apparente pendant une durée significative. En effet, l'utilisateur est capable de détecter un ralentissement s'il dure plus de 100 ms, ce qui est un laps de temps très court. Pis encore, Android lui-même peut déceler quand votre application n'est pas assez réactive, auquel cas il lancera une boîte de dialogue qui s'appelle ANR et qui incitera l'utilisateur à quitter l'application. Il existe deux évènements qui peuvent lancer des ANR :

  1. L'application ne répond pas à une impulsion de l'utilisateur sur l'interface graphique en moins de cinq secondes.
  2. Un Broadcast Receiver s'exécute en plus de dix secondes.

C'est pourquoi nous allons voir ici comment faire exécuter du travail en arrière-plan, de façon à exécuter du code en parallèle de votre interface graphique, pour ne pas la bloquer quand on veut effectuer de grosses opérations qui risqueraient d'affecter l'expérience de l'utilisateur.

La gestion du multitâche par Android

Comme vous le savez, un programme informatique est constitué d'instructions destinées au processeur. Ces instructions sont présentées sous la forme d'un code, et lors de l'exécution de ce code, les instructions sont traitées par le processeur dans un ordre précis.

Tous les programmes Android s'exécutent dans ce qu'on appelle un processus. On peut définir un processus comme une instance d'un programme informatique qui est en cours d'exécution. Il contient non seulement le code du programme, mais aussi des variables qui représentent son état courant. Parmi ces variables s'en trouvent certaines qui permettent de définir la plage mémoire qui est mise à la disposition du processus.

Pour être exact, ce n'est pas le processus en lui-même qui va exécuter le code, mais l'un de ses constituants. Les constituants destinés à exécuter le code s'appellent des threads (« fils d'exécution » en français). Dans le cas d'Android, les threads sont contenus dans les processus. Un processus peut avoir un ou plusieurs threads, par conséquent un processus peut exécuter plusieurs portions du code en parallèle s'il a plusieurs threads. Comme un processus n'a qu'une plage mémoire, alors tous les threads se partagent les accès à cette plage mémoire. On peut voir à la figure suivante deux processus. Le premier possède deux threads, le second en possède un seul. On peut voir qu'il est possible de communiquer entre les threads ainsi qu'entre les processus.

Schéma de fonctionnement des threads

Vous vous rappelez qu'une application Android est constituée de composants, n'est-ce pas ? Nous n'en connaissons que deux types pour l'instant, les activités et les receivers. Il peut y avoir plusieurs de ces composants dans une application. Dès qu'un composant est lancé (par exemple au démarrage de l'application ou quand on reçoit un Broadcast Intent dans un receiver), si cette application n'a pas de processus fonctionnel, alors un nouveau sera créé. Tout processus nouvellement créé ne possède qu'un thread. Ce thread s'appelle le thread principal.

En revanche, si un composant démarre alors qu'il y a déjà un processus pour cette application, alors le composant se lancera dans le processus en utilisant le même thread.

Processus

Par défaut, tous les composants d'une même application se lancent dans le même processus, et d'ailleurs c'est suffisant pour la majorité des applications. Cependant, si vous le désirez et si vous avez une raison bien précise de le faire, il est possible de définir dans quel processus doit se trouver tel composant de telle application à l'aide de la déclaration du composant dans le Manifest. En effet, l'attribut android:process permet de définir le processus dans lequel ce composant est censé s'exécuter, afin que ce composant ne suive pas le même cycle de vie que le restant de l'application. De plus, si vous souhaitez qu'un composant s'exécute dans un processus différent mais reste privé à votre application, alors rajoutez « : » à la déclaration du nom du processus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<activity
  android:name=".SensorsActivity"
  android:label="@string/app_name" >
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>

<activity
  android:name=".DetailActivity" >
</activity>

<receiver
  android:name=".LowBatteryReceiver"
  android:process=":sdz.chapitreQuatre.process.deux" >
  <intent-filter>
    <action android:name="android.intent.action.BATTERY_LOW" />
  </intent-filter>
</receiver>

Ici, j'ai un receiver qui va s'enclencher dès que la batterie devient faible. Configuré de cette manière, mon receiver ne pourra démarrer que si l'application est lancée (comme j'ai rajouté « : », seule mon application pourra le lancer) ; cependant, si l'utilisateur ferme l'application alors que le receiver est en route, le receiver ne s'éteindra pas puisqu'il se trouvera dans un autre processus que le restant des composants.

Quand le système doit décider quel processus il doit tuer, pour libérer de la mémoire principalement, il mesure quelle est l'importance relative de chaque processus pour l'utilisateur. Par exemple, il sera plus enclin à fermer un processus qui ne contient aucune activité visible pour l'utilisateur, alors que d'autres ont des composants qui fonctionnent encore — une activité visible ou un receiver qui gère un évènement. On dit qu'une activité visible a une plus grande priorité qu'une activité non visible.

Threads

Quand une activité est lancée, le système crée un thread principal dans lequel s'exécutera l'application. C'est ce thread qui est en charge d'écouter les évènements déclenchés par l'utilisateur quand il interagit avec l'interface graphique. C'est pourquoi le second nom du thread principal est thread UI (UI pour User Interface, « interface utilisateur » en français).

Mais il est possible d'avoir plusieurs threads. Android utilise un pool de threads (comprendre une réserve de threads, pas une piscine de threads :p ) pour gérer le multitâche. Un pool de threads comprend un certain nombre n de threads afin d'exécuter un certain nombre m de tâches (n et m n'étant pas forcément identiques) qui se trouvent dans un autre pool en attendant qu'un thread s'occupe d'elles. Logiquement, un pool est organisé comme une file, ce qui signifie qu'on empile les éléments les uns sur les autres et que nous n'avons accès qu'au sommet de cet empilement. Les résultats de chaque thread sont aussi placés dans un pool de manière à pouvoir les récupérer dans un ordre cohérent. Dès qu'un thread complète sa tâche, il va demander la prochaine tâche qui se trouve dans le pool jusqu'à ce qu'il n'y ait plus de tâches.

Avant de continuer, laissez-moi vous expliquer le fonctionnement interne de l'interface graphique. Dès que vous effectuez une modification sur une vue, que ce soit un widget ou un layout, cette modification ne se fait pas instantanément. À la place, un évènement est créé. Il génère un message, qui sera envoyé dans une pile de messages. L'objectif du thread UI est d'accéder à la pile des messages, de dépiler le premier message à traiter, de le traiter, puis de passer au suivant. De plus, ce thread s'occupe de toutes les méthodes de callback du système, par exemple onCreate() ou onKeyDown(). Si le système est occupé à un travail intensif, il ne pourra pas traiter les méthodes de callback ou les interactions avec l'utilisateur. De ce fait, un ARN est déclenché pour signaler à l'utilisateur qu'Android n'est pas d'accord avec ce comportement.

De la sorte, il faut respecter deux règles dès qu'on manipule des threads :

  1. Ne jamais bloquer le thread UI.
  2. Ne pas manipuler les vues standards en dehors du thread UI.

Enfin, on évite certaines opérations dans le thread UI, en particulier :

  • Accès à un réseau, même s'il s'agit d'une courte opération en théorie.
  • Certaines opérations dans la base de données, surtout les sélections multiples.
  • Les accès fichiers, qui sont des opérations plutôt lentes.
  • Enfin, les accès matériels, car certains demandent des temps de chargement vraiment trop longs.

Mais voyons un peu les techniques qui nous permettrons de faire tranquillement ces opérations.

Gérer correctement les threads simples

La base

En Java, un thread est représenté par un objet de type Thread, mais avant cela laissez-moi vous parler de l'interface Runnable. Cette interface représente les objets qui sont capables de faire exécuter du code au processeur. Elle ne possède qu'une méthode, void run(), dans laquelle il faut écrire le code à exécuter.

Ainsi, il existe deux façons d'utiliser les threads. Comme Thread implémente Runnable, alors vous pouvez très bien créer une classe qui dérive de Thread afin de redéfinir la méthode void run(). Par exemple, ce thread fait en sorte de chercher un texte dans un livre pour le mettre dans un TextView :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ChercherTexte extends Thread {
  // La phrase à chercher dans le texte
  public String a_chercher = "Être ou ne pas être";
  // Le livre
  public String livre;
  // Le TextView dans lequel mettre le résultat
  public TextView display;

  public void run() {
    int caractere = livre.indexOf(a_chercher);
    display.setText("Cette phrase se trouve au " + caractere + " ème caractère.");
  }
}

Puis on ajoute le Thread à l'endroit désiré et on le lance avec synchronized void start () :

1
2
3
4
5
6
7
8
public void onClick(View v) {
  Thread t = new Thread();

  t.livre = hamlet;
  t.display = v;

  t.start();
}

Une méthode synchronized a un verrou. Dès qu'on lance cette méthode, alors le verrou s'enclenche et il est impossible pour d'autres threads de lancer la même méthode.

Mais ce n'est pas la méthode à privilégier, car elle est contraignante à entretenir. À la place, je vous conseille de passer une instance anonyme de Runnable dans un Thread :

1
2
3
4
5
6
7
8
public void onClick(View v) {
  new Thread(new Runnable() {
    public void run() {
      int caractere = hamlet.indexOf("Être ou ne pas être");
      v.setText("Cette phrase se trouve au " + caractere + " ème caractère.");
    }
  }).start();
}

Le problème de notre exemple, c'est que l'opération coûteuse (la recherche d'un texte dans un livre) s'exécute dans un autre thread. C'est une bonne chose, c'est ce qu'on avait demandé, comme ça la recherche se fait sans bloquer le thread UI, mais on remarquera que la vue est aussi manipulée dans un autre thread, ce qui déroge à la seconde règle vue précédemment, qui précise que les vues doivent être manipulées dans le thread UI ! On risque de rencontrer des comportements inattendus ou impossibles à prédire !

Afin de remédier à ce problème, Android offre plusieurs manières d’accéder au thread UI depuis d'autres threads. Par exemple :

  • La méthode d'Activity void runOnUiThread(Runnable action) spécifie qu'une action doit s'exécuter dans le thread UI. Si le thread actuel est le thread UI, alors l'action est exécutée immédiatement. Sinon, l'action est ajoutée à la pile des évènements du thread UI.
  • Sur un View, on peut faire boolean post(Runnable action) pour ajouter le Runnable à la pile des messages du thread UI. Le boolean retourné vaut true s'il a été correctement placé dans la pile des messages.
  • De manière presque similaire, boolean postDelayed(Runnable action, long delayMillis) permet d'ajouter un Runnable à la pile des messages, mais uniquement après qu'une certaine durée delayMillis s'est écoulée.

On peut par exemple voir :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void onClick(View v) {
  new Thread(new Runnable() {
    public void run() {
      int caractere = hamlet.indexOf("Être ou ne pas être");
      v.post(new Runnable() {
        public void run() {
          v.setText("Cette phrase se trouve au " + caractere + " ème caractère.");
        }
      });
    }
  }).start();
}

Ou :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void onClick(View v) {
  new Thread(new Runnable() {
    public void run() {
      int caractere = hamlet.indexOf("Être ou ne pas être");
      runOnUiThread(new Runnable() {
        public void run() {
          v.setText("Cette phrase se trouve au " + caractere + " ème caractère.");
        }
      });
    }
  }).start();
}

Ainsi, la longue opération s'exécute dans un thread différent, ce qui est bien, et la vue est manipulée dans le thread UI, ce qui est parfait !

Le problème ici est que ce code peut vite devenir difficile à maintenir. Vous avez vu, pour à peine deux lignes de code à exécuter, on a dix lignes d'enrobage ! Je vais donc vous présenter une solution qui permet un contrôle total tout en étant plus évidente à manipuler.

Les messages et les handlers

La classe Thread est une classe de bas niveau et en Java on préfère travailler avec des objets d'un plus haut niveau. Une autre manière d'utiliser les threads est d'utiliser la classe Handler, qui est d'un plus haut niveau.

En informatique, « de haut niveau » signifie « qui s'éloigne des contraintes de la machine pour faciliter sa manipulation pour les humains ».

La classe Handler contient un mécanisme qui lui permet d'ajouter des messages ou des Runnable à une file de messages. Quand vous créez un Handler, il sera lié à un thread, c'est donc dans la file de ce thread-là qu'il pourra ajouter des messages. Le thread UI possède lui aussi un handler et chaque handler que vous créerez communiquera par défaut avec ce handler-là.

Les handlers tels que je vais les présenter doivent être utilisés pour effectuer des calculs avant de mettre à jour l'interface graphique, et c'est tout. Ils peuvent être utilisés pour effectuer des calculs et ne pas mettre à jour l'interface graphique après, mais ce n'est pas le comportement attendu.

Mais voyons tout d'abord comment les handlers font pour se transmettre des messages. Ces messages sont représentés par la classe Message. Un message peut contenir un Bundle avec la méthode void setData(Bundle data). Mais comme vous le savez, un Bundle, c'est lourd, il est alors possible de mettre des objets dans des attributs publics :

  • arg1 et arg2 peuvent contenir des entiers.
  • On peut aussi mettre un Object dans obj.

Bien que le constructeur de Message soit public, la meilleure manière d'en construire un est d'appeler la méthode statique Message Message.obtain() ou encore Message Handler.obtainMessage(). Ainsi, au lieu d'allouer de nouveaux objets, on récupère des anciens objets qui se trouvent dans le pool de messages. Notez que si vous utilisez la seconde méthode, le handler sera déjà associé au message, mais vous pouvez très bien le mettre a posteriori avec void setTarget(Handler target).

1
2
3
Message msg = Handler.obtainMessage();
msg.arg1 = 25;
msg.obj = new String("Salut !");

Enfin, les méthodes pour planifier des messages sont les suivantes :

  • boolean post(Runnable r) pour ajouter r à la queue des messages. Il s'exécutera sur le Thread auquel est rattaché le Handler. La méthode renvoie true si l'objet a bien été rajouté. De manière alternative, boolean postAtTime(Runnable r, long uptimeMillis) permet de lancer un Runnable au moment longMillis et boolean postDelayed(Runnable r, long delayMillis) permet d'ajouter un Runnable à lancer après un délai de delayMillis.
  • boolean sendEmptyMessage(int what) permet d'envoyer un Message simple qui ne contient que la valeur what, qu'on peut utiliser comme un identifiant. On trouve aussi les méthodes boolean sendEmptyMessageAtTime(int what, long uptimeMillis) et boolean sendEmptyMessageDelayed(int what, long delayMillis).
  • Pour pousser un Message complet à la fin de la file des messages, utilisez boolean sendMessage(Message msg). On trouve aussi boolean sendMessageAtTime(Message msg, long uptimeMillis) et boolean sendMessageDelayed(Message msg, long delayMillis).

Tous les messages seront reçus dans la méthode de callback void handleMessage(Message msg) dans le thread auquel est attaché ce Handler.

1
2
3
4
5
6
public class MonHandler extends Handler {
  @Override
  public void handleMessage(Message msg) {
    // Faire quelque chose avec le message
  }
}

Application : une barre de progression

Énoncé

Une utilisation typique des handlers est de les incorporer dans la gestion des barres de progression. On va faire une petite application qui ne possède au départ qu'un bouton. Cliquer dessus lance un téléchargement et une boîte de dialogue s'ouvrira. Cette boîte contiendra une barre de progression qui affichera l'avancement du téléchargement, comme à la figure suivante. Dès que le téléchargement se termine, la boîte de dialogue se ferme et un Toast indique que le téléchargement est terminé. Enfin, si l'utilisateur s'impatiente, il peut très bien fermer la boîte de dialogue avec le bouton Retour.

Une barre de progression

Spécifications techniques

On va utiliser un ProgressDialog pour afficher la barre de progression. Il s'utilise comme n'importe quelle boîte de dialogue, sauf qu'il faut lui attribuer un style si on veut qu'il affiche une barre de progression. L'attribution se fait avec la méthode setProgressStyle(int style) en lui passant le paramètre ProgressDialog.STYLE_HORIZONTAL.

L'état d'avancement sera conservé dans un attribut. Comme on ne sait pas faire de téléchargement, l'avancement se fera au travers d'une boucle qui augmentera cet attribut. Bien entendu, on ne fera pas cette boucle dans le thread principal, sinon l'interface graphique sera complètement bloquée ! Alors on lancera un nouveau thread. On passera par un handler pour véhiculer des messages. On répartit donc les rôles ainsi :

  • Dans le nouveau thread, on calcule l'état d'avancement, puis on l'envoie au handler à l'aide d'un message.
  • Dans le handler, dès qu'on reçoit le message, on met à jour la progression de la barre.

Entre chaque incrémentation de l'avancement, allouez-vous une seconde de répit, sinon votre téléphone va faire la tête. On peut le faire avec :

1
2
3
4
5
try {
  Thread.sleep(1000);
} catch (InterruptedException e) {
  e.printStackTrace();
}

Enfin, on peut interrompre un Thread avec la méthode void interrupt(). Cependant, si votre thread est en train de dormir à cause de la méthode sleep, alors l'interruption InterruptedException sera lancée et le thread ne s'interrompra pas. À vous de réfléchir pour contourner ce problème.

Ma solution

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import android.app.Activity;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class ProgressBarActivity extends Activity {
  private final static int PROGRESS_DIALOG_ID = 0;
  private final static int MAX_SIZE = 100;
  private final static int PROGRESSION = 0;

  private Button mProgressButton = null;
  private ProgressDialog mProgressBar = null;
  private Thread mProgress = null;

  private int mProgression = 0;

  // Gère les communications avec le thread de téléchargement
  final private Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
      super.handleMessage(msg);
      // L'avancement se situe dans msg.arg1
      mProgressBar.setProgress(msg.arg1);
    }
  };

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_progress_bar);

    mProgressButton = (Button) findViewById(R.id.progress_button);
    mProgressButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        // Initialise la boîte de dialogue
        showDialog(PROGRESS_DIALOG_ID);

        // On remet le compteur à zéro
        mProgression = 0;

        mProgress = new Thread(new Runnable() {
          public void run() {
            try {
              while (mProgression < MAX_SIZE) {
                // On télécharge un bout du fichier
                mProgression = download();

                // Repose-toi pendant une seconde    
                Thread.sleep(1000);

                Message msg = mHandler.obtainMessage(PROGRESSION, mProgression, 0);
                mHandler.sendMessage(msg);
              }

              // Le fichier a été téléchargé
              if (mProgression >= MAX_SIZE) {
                runOnUiThread(new Runnable() {
                  @Override
                  public void run() {
                    Toast.makeText(ProgressBarActivity.this, ProgressBarActivity.this.getString(R.string.over), Toast.LENGTH_SHORT).show();
                  }
                });

                // Ferme la boîte de dialogue
                mProgressBar.dismiss();
              }
            } catch (InterruptedException e) {
              // Si le thread est interrompu, on sort de la boucle de cette manière
              e.printStackTrace();
            }
          }
        }).start();
      }
    });
  }

  @Override
  public Dialog onCreateDialog(int identifiant) {
    if(mProgressBar == null) {
      mProgressBar = new ProgressDialog(this);
      // L'utilisateur peut annuler la boîte de dialogue
      mProgressBar.setCancelable(true);
      // Que faire quand l'utilisateur annule la boîte ?
      mProgressBar.setOnCancelListener(new DialogInterface.OnCancelListener() {
        @Override
        public void onCancel(DialogInterface dialog) {
          // On interrompt le thread  
          mProgress.interrupt();
          Toast.makeText(ProgressBarActivity.this, ProgressBarActivity.this.getString(R.string.canceled), Toast.LENGTH_SHORT).show();
          removeDialog(PROGRESS_DIALOG_ID);
        }
      });
      mProgressBar.setTitle("Téléchargement en cours");
      mProgressBar.setMessage("C'est long...");
      mProgressBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
      mProgressBar.setMax(MAX_SIZE);
    }
    return mProgressBar;
  }

  public int download() {
    if(mProgression <= MAX_SIZE) {
      mProgression++;
      return mProgression;
    }
    return MAX_SIZE;
  }
}

Sécuriser ses threads

Les threads ne sont pas des choses aisées à manipuler. À partir de notre application précédente, nous allons voir certaines techniques qui vous permettront de gérer les éventuels débordements imputés aux threads.

Il y a une fuite

Une erreur que nous avons commise est d'utiliser le handler en classe interne. Le problème de cette démarche est que quand on déclare une classe interne, alors chaque instance de cette classe contient une référence à la classe externe. Par conséquent, tant qu'il y a des messages sur la pile des messages qui sont liés au handler, l'activité ne pourra pas être nettoyée par le système, et une activité, ça pèse lourd pour le système !

Une solution simple est de faire une classe externe qui dérive de Handler, et de rajouter une instance de cette classe en tant qu'attribut.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import android.app.ProgressDialog;
import android.os.Handler;
import android.os.Message;

public class ProgressHandler extends Handler {    
  @Override
  public void handleMessage(Message msg) {
    super.handleMessage(msg);
    ProgressDialog progressBar = (ProgressDialog)msg.obj;
    progressBar.setProgress(msg.arg1);
  }
}

Ne pas oublier d'inclure la boîte de dialogue dans le message puisque nous ne sommes plus dans la même classe ! Si vous vouliez vraiment rester dans la même classe, alors vous auriez pu déclarer ProgressHandler comme statique de manière à séparer les deux classes.

Gérer le cycle de l'activité

Il faut lier les threads au cycle des activités. On pourrait se dire qu'on veut parfois effectuer des tâches d'arrière-plan même quand l'activité est terminée, mais dans ce cas-là on ne passera pas par des threads mais par des Services, qui seront étudiés dans le prochain chapitre.

Le plus important est de gérer le changement de configuration. Pour cela, tout se fait dans onRetainNonConfigurationInstance(). On fait en sorte de sauvegarder le thread ainsi que la boîte de dialogue de manière à pouvoir les récupérer :

1
2
3
4
5
6
public Object onRetainNonConfigurationInstance () {
  List<Object> list = new ArrayList<Object>();
  list.add(mProgressBar);
  list.add(mProgress);
  return list;
}

Enfin, vous pouvez aussi faire en sorte d'arrêter le thread dès que l'activité passe en pause ou quitte son cycle.

AsyncTask

Il faut avouer que tout cela est bien compliqué et nécessite de penser à tout, ce qui est source de confusion. Je vais donc vous présenter une alternative plus évidente à maîtriser, mais qui est encore une fois réservée à l’interaction avec le thread UI. AsyncTask vous permet de faire proprement et facilement des opérations en parallèle du thread UI. Cette classe permet d'effectuer des opérations d'arrière-plan et de publier les résultats dans le thread UI sans avoir à manipuler de threads ou de handlers.

AsyncTask n'est pas une alternative radicale à la manipulation des threads, juste un substitut qui permet le travail en arrière-plan sans toucher les blocs de bas niveau comme les threads. On va surtout les utiliser pour les opérations courtes (quelques secondes tout au plus) dont on connaît précisément l'heure de départ et de fin.

On ne va pas utiliser directement AsyncTask, mais plutôt créer une classe qui en dérivera. Cependant, il ne s'agit pas d'un héritage évident puisqu'il faut préciser trois paramètres :

  • Le paramètre Params permet de définir le typage des objets sur lesquels on va faire une opération.
  • Le deuxième paramètre, Progress, indique le typage des objets qui indiqueront l'avancement de l'opération.
  • Enfin, Result est utilisé pour symboliser le résultat de l'opération.

Ce qui donne dans le contexte :

1
public class MaClasse extends AsyncTask<Params, Progress, Result>

Ensuite, pour lancer un objet de type MaClasse, il suffit d'utiliser dessus la méthode final AsyncTask<Params, Progress, Result> execute (Params... params) sur laquelle il est possible de faire plusieurs remarques :

  • Son prototype est accompagné du mot-clé final, ce qui signifie que la méthode ne peut être redéfinie dans les classes dérivées d'AsyncTask.
  • Elle prend un paramètre de type Params... ce qui pourra en troubler plus d'un, j'imagine. Déjà, Params est tout simplement le type que nous avons défini auparavant dans la déclaration de MaClasse. Ensuite, les trois points signifient qu'il s'agit d'arguments variables et que par conséquent on peut en mettre autant qu'on veut. Si on prend l'exemple de la méthode int somme(int... nombres), on peut l'appeler avec somme(1) ou somme(1,5,-2). Pour être précis, il s'agit en fait d'un tableau déguisé, vous pouvez donc considérer nombres comme un int[].

Une fois cette méthode exécutée, notre classe va lancer quatre méthodes de callback, dans cet ordre :

  • void onPreExecute() est lancée dès le début de l'exécution, avant même que le travail commence. On l'utilise donc pour initialiser les différents éléments qui doivent être initialisés.
  • Ensuite, on trouve Result doInBackground(Params... params), c'est dans cette méthode que doit être effectué le travail d'arrière-plan. À la fin, on renvoie le résultat de l'opération et ce résultat sera transmis à la méthode suivante — on utilise souvent un boolean pour signaler la réussite ou l'échec de l'opération. Si vous voulez publier une progression pendant l'exécution de cette méthode, vous pouvez le faire en appelant final void publishProgress(Progress... values) (la méthode de callback associée étant void onProgressUpdate(Progress... values)).
  • Enfin, void onPostExecute(Result result) permet de conclure l'utilisation de l'AsyncTask en fonction du résultat result passé en paramètre.

De plus, il est possible d'annuler l'action d'un AsyncTask avec final boolean cancel(boolean mayInterruptIfRunning), où mayInterruptIfRunning vaut true si vous autorisez l'exécution à s'interrompre. Par la suite, une méthode de callback est appelée pour que vous puissez réagir à cet évènement : void onCancelled().

Enfin, dernière chose à savoir, un AsyncTask n'est disponible que pour une unique utilisation, s'il s'arrête ou si l'utilisateur l'annule, alors il faut en recréer un nouveau.

Et cette fois on fait comment pour gérer les changements de configuration ?

Ah ! vous aimez avoir mal, j'aime ça. :pirate: Accrochez-vous parce que ce n'est pas simple. Ce que nous allons voir est assez avancé et de bas niveau, alors essayez de bien comprendre pour ne pas faire de boulettes quand vous l'utiliserez par la suite.

On pourrait garder l'activité qui a lancé l'AsyncTask en paramètre, mais de manière générale il ne faut jamais garder de référence à une classe qui dérive de Context, par exemple Activity. Le problème, c'est qu'on est bien obligés par moment. Alors comment faire ?

Revenons aux bases de la programmation. Quand on crée un objet, on réserve dans la mémoire allouée par le processus un emplacement qui aura la place nécessaire pour mettre l'objet. Pour accéder à l'objet, on utilise une référence sous forme d'un identifiant déclaré dans le code :

1
String chaine = new String();

Ici, chaine est l'identifiant, autrement dit une référence qui pointe vers l'emplacement mémoire réservé pour cette chaîne de caractères.

Bien sûr, au fur et à mesure que le programme s'exécute, on va allouer de la place pour d'autres objets et, si on ne fait rien, la mémoire va être saturée. :waw: Afin de faire en sorte de libérer de la mémoire, un processus qui s'appelle le garbage collector (« ramasse-miettes » en français) va détruire les objets qui ne sont plus susceptibles d'être utilisés :

1
2
3
4
5
String chaine = new String("Rien du tout");

if(chaine.equals("Quelque chose") {
  int dix = 10;
}

La variable chaine sera disponible avant, pendant et après le if puisqu'elle a été déclarée avant (donc de 1 à 5, voire plus loin encore), en revanche dix a été déclaré dans le if, il ne sera donc disponible que dedans (donc de 4 à 5). Dès qu'on sort du if, le garbage collector passe et désalloue la place réservée dix de manière à pouvoir l'allouer à un autre objet.

Quand on crée un objet en Java, il s'agit toujours d'une référence forte, c'est-à-dire que l'objet est protégé contre le garbage collector tant qu'on est certain que vous l'utilisez encore. Ainsi, si on garde notre activité en référence forte en tant qu'attribut de classe, elle restera toujours disponible, et vous avez bien compris que ce n'était pas une bonne idée, surtout qu'une référence à une activité est bien lourde.

À l'opposé des références fortes se trouvent les références faibles. Les références faibles ne protègent pas une référence du garbage collector.

Ainsi, si vous avez une référence forte vers un objet, le garbage collector ne passera pas dessus. Si vous en avez deux, idem. Si vous avez deux références fortes et une référence faible, c'est la même chose, parce qu'il y a deux références fortes.

Si le garbage collector réalise que l'une des deux références fortes n'est plus valide, l'objet est toujours conservé en mémoire puisqu'il reste une référence forte. En revanche, dès que la seconde référence forte est invalidée, alors l'espace mémoire est libéré puisqu'il ne reste plus aucune référence forte, juste une petite référence faible qui ne protège pas du ramasse-miettes.

Ainsi, il suffit d'inclure une référence faible vers notre activité dans l'AsyncTask pour pouvoir garder une référence vers l'activité sans pour autant la protéger contre le ramasse-miettes.

Pour créer une référence faible d'un objet T, on utilise WeakReference de cette manière :

1
2
T strongReference = new T();
WeakReference<T> weakReference = new WeakReference<T>(strongReference);

Il n'est bien entendu pas possible d'utiliser directement un WeakReference, comme il ne s'agit que d'une référence faible, il vous faut donc récupérer une référence forte de cet objet. Pour ce faire, il suffit d'utiliser T get(). Cependant, cette méthode renverra null si l'objet a été nettoyé par le garbage collector.

Application

Énoncé

Faites exactement comme l'application précédente, mais avec un AsyncTask cette fois.

Spécifications techniques

L'AsyncTask est utilisé en tant que classe interne statique, de manière à ne pas avoir de fuites comme expliqué dans la partie consacrée aux threads.

Comme un AsyncTask n'est disponible qu'une fois, on va en recréer un à chaque fois que l'utilisateur appuie sur le bouton.

Il faut lier une référence faible à votre activité à l'AsyncTask pour qu'à chaque fois que l'activité est détruite on reconstruise une nouvelle référence faible à l'activité dans l'AsyncTask. Un bon endroit pour faire cela est dans le onRetainNonConfigurationInstance().

Ma solution

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package sdz.chapitreQuatre.async.example;

import java.lang.ref.WeakReference;

import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class AsyncActivity extends Activity {
  // Taille maximale du téléchargement
  public final static int MAX_SIZE = 100;
  // Identifiant de la boîte de dialogue
  public final static int ID_DIALOG = 0;
  // Bouton qui permet de démarrer le téléchargement
  private Button mBouton = null;

  private ProgressTask mProgress = null;
  // La boîte en elle-même
  private ProgressDialog mDialog = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mBouton = (Button) findViewById(R.id.bouton);
    mBouton.setOnClickListener(new View.OnClickListener() {

      @Override
      public void onClick(View arg0) {
        // On recrée à chaque fois l'objet
        mProgress = new ProgressTask(AsyncActivity.this);
        // On l'exécute
        mProgress.execute();
      }
    });

    // On recupère l'AsyncTask perdu dans le changement de configuration
    mProgress = (ProgressTask) getLastNonConfigurationInstance();

    if(mProgress != null)
      // On lie l'activité à l'AsyncTask
      mProgress.link(this);
  }

  @Override
  protected Dialog onCreateDialog (int id) {
    mDialog = new ProgressDialog(this);
    mDialog.setCancelable(true);
    mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
      @Override
      public void onCancel(DialogInterface arg0) {
        mProgress.cancel(true);
      }
    });
    mDialog.setTitle("Téléchargement en cours");
    mDialog.setMessage("C'est long...");
    mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
    mDialog.setMax(MAX_SIZE);
    return mDialog;
  }

  @Override
  public Object onRetainNonConfigurationInstance () {
    return mProgress;
  }

  // Met à jour l'avancement dans la boîte de dialogue
  void updateProgress(int progress) {
    mDialog.setProgress(progress);
  }

  // L'AsyncTask est bien une classe interne statique
  static class ProgressTask extends AsyncTask<Void, Integer, Boolean> {
    // Référence faible à l'activité
    private WeakReference<AsyncActivity> mActivity = null;
    // Progression du téléchargement
    private int mProgression = 0;

    public ProgressTask (AsyncActivity pActivity) {
      link(pActivity);
    }

    @Override
    protected void onPreExecute () {
      // Au lancement, on affiche la boîte de dialogue
      if(mActivity.get() != null)
        mActivity.get().showDialog(ID_DIALOG);
    }

    @Override
    protected void onPostExecute (Boolean result) {
      if (mActivity.get() != null) {
        if(result)
          Toast.makeText(mActivity.get(), "Téléchargement terminé", Toast.LENGTH_SHORT).show();
        else
          Toast.makeText(mActivity.get(), "Echec du téléchargement", Toast.LENGTH_SHORT).show();
      }
    }

    @Override
    protected Boolean doInBackground (Void... arg0) {
      try {
        while(download() != MAX_SIZE) {
          publishProgress(mProgression);
          Thread.sleep(1000);
        }
        return true;
      }catch(InterruptedException e) {
        e.printStackTrace();
        return false;
      }
    }

    @Override
    protected void onProgressUpdate (Integer... prog) {
      // À chaque avancement du téléchargement, on met à jour la boîte de dialogue
      if (mActivity.get() != null)
        mActivity.get().updateProgress(prog[0]);
    }

    @Override
    protected void onCancelled () {
      if(mActivity.get() != null)
        Toast.makeText(mActivity.get(), "Annulation du téléchargement", Toast.LENGTH_SHORT).show();
    }

    public void link (AsyncActivity pActivity) {
      mActivity = new WeakReference<AsyncActivity>(pActivity);
    }

    public int download() {
      if(mProgression <= MAX_SIZE) {
        mProgression++;
        return mProgression;
      }
      return MAX_SIZE;
    }
  }
}

Pour terminer, voici une liste de quelques comportements à adopter afin d'éviter les aléas des blocages :

  • Si votre application fait en arrière-plan de gros travaux bloquants pour l'interface graphique (imaginez qu'elle doive télécharger une image pour l'afficher à l'utilisateur), alors il suffit de montrer l'avancement de ce travail avec une barre de progression de manière à faire patienter l'utilisateur.
  • En ce qui concerne les jeux, usez et abusez des threads pour effectuer des calculs de position, de collision, etc.
  • Si votre application a besoin de faire des initialisations au démarrage et que par conséquent elle met du temps à se charger, montrez un splash screen avec une barre de progression pour montrer à l'utilisateur que votre application n'est pas bloquée.

  • Par défaut, au lancement d'une application, le système l'attache à un nouveau processus dans lequel il sera exécuté. Ce processus contiendra tout un tas d'informations relatives à l'état courant de l'application qu'il contient et des threads qui exécutent le code.
  • Vous pouvez décider de forcer l'exécution de certains composants dans un processus à part grâce à l'attribut android:process à rajouter dans l'un des éléments constituant le noeud <application> de votre manifest.
  • Lorsque vous jouez avec les threads, vous ne devez jamais perdre à l'esprit deux choses :
    • Ne jamais bloquer le thread UI.
    • Ne pas manipuler les vues standards en dehors du thread UI.
  • On préfèrera toujours privilégier les concepts de haut niveau pour faciliter les manipulations pour l'humain et ainsi donner un niveau d'abstraction aux contraintes machines. Pour les threads, vous pouvez donc privilégier les messages et les handlers à l'utilisation directe de la classe Thread.
  • AsyncTask est un niveau d'abstraction encore supérieur aux messages et handlers. Elle permet d'effectuer des opérations en arrière-plan et de publier les résultats dans le thread UI facilement grâce aux méthodes qu'elle met à disposition des développeurs lorsque vous en créez une.