Communication inter-threads par message
Thread principale
- Par défaut, une application Android n'utilise qu'une seule thread : la thread principale
- Cette thread gère les événements de l'activité (cycle de vie : onStart(), onStop()...) ainsi que toute la gestion graphique
-
La thread principale reçoit des messages pour les événements :
- ces messages sont envoyés par le système (événements frappe clavier, curseur...)
- mais aussi possiblement par d'autres threads de l'application (threads secondaire)
-
Il est interdit de toucher à l'interface graphique en dehors de la thread principale
- Si une thread secondaire souhaite modifier l'UI, elle doit envoyer un message à la thread principale pour qu'elle exécute le code de modification
- La thread principale gère une file de messages avec une boucle récupérant les messages et réagissant à ceux-ci (appels de listeners)
- Il faut éviter l'utilisation de verrous et moniteurs et privilégier la communication par messages entre threads
File de messages
- File de message implantée avec Looper
-
Un Looper est associé à un handler chargé de traiter les messages :
- Construction : Handler(Looper looper, Handler.Callback callback)
- Méthode handleMessage(Message) de Handler.Callback pour traiter chaque message reçu
-
On peut utiliser un Handler pour envoyer des messages sur un Looper
- Construction : Handler() (par défaut le Handler est connecté au Looper de la thread courante)
-
Envoi de message ou Runnable :
- sendMessage(Message msg)
- post(Runnable r)
- Utilisation d'un délai : ...AtTime(..., long millisTime), ...Delayed(..., long millisDelay)
- Placement en tête de file : ...AtFontOfQueue(...)
- Création d'un message et envoi : Message msg = handler.obtainMessage(intCode, objectToInclude); msg.sendToTarget()
Pratique courante :
- Créer un Handler comme champ d'une activité à partir la méthode onCreate() (lié à la thread principale)
- Utiliser ce champ dans les threads secondaires pour envoyer des messages
Exemple #1 : affichage d'un compteur mis à jour chaque seconde en utilisant une thread secondaire
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.counter; import android.os.Handler; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import fr.upem.coursand.R; public class ThreadCounterActivity extends AppCompatActivity { private TextView counterView; private Handler handler; private Thread refreshThread = null; public void onCreate(Bundle b) { super.onCreate(b); setContentView(R.layout.activity_thread_counter); handler = new Handler(); counterView = findViewById(R.id.counterView); } /** * we start here the incrementing thread */ @Override protected void onStart() { super.onStart(); refreshThread = new Thread( ()-> { int counter = 0; while (!Thread.interrupted()) { // UI code must be executed in the UI thread (we post the code on the handler) final int counterNow = counter; handler.post( () -> counterView.setText("" + counterNow)); // we could also use this // runOnUiThread( () -> { counterView.setText("" + counter); } ); try { Thread.sleep(1000); } catch (InterruptedException e) { return; // exit from the thread } counter++; } }); refreshThread.start(); } /** * we stop here the incrementing thread */ @Override public void onStop() { super.onStop(); // testing nullity is a precaution (refreshThread should be not null) if (refreshThread != null) refreshThread.interrupt(); } }
Exemple #2 : affichage d'un compteur incrémenté sans utiliser de thread secondaire
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.counter; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.os.Handler; import android.widget.TextView; import fr.upem.coursand.R; /** * Example activity incrementing a counter without using an additional thread * (only a Handler is employed) */ public class HandlerCounterActivity extends AppCompatActivity { private TextView counterView; private Handler handler; private long counterNow = 0L; public void onCreate(Bundle b) { super.onCreate(b); setContentView(R.layout.activity_thread_counter); // use the same layout than the thread version handler = new Handler(); counterView = findViewById(R.id.counterView); } /** Code executed periodically to display and increment the counter */ private final Runnable incrementRunnable = () -> { counterView.setText("" + counterNow++); // recursive call of increment runnable 1 second later handler.postDelayed(getIncrementRunnable(), 1000); }; /** Required to allow recursive call from increment runnable */ private Runnable getIncrementRunnable() { return incrementRunnable; } /** * we start here the incrementation */ @Override protected void onStart() { super.onStart(); incrementRunnable.run(); } /** * we stop here the incrementation */ @Override public void onStop() { super.onStop(); handler.removeCallbacks(incrementRunnable); } }
Remarques :
- Le code exécuté à la suite d'un événément sur la thread principale doit toujours être rapide.
- Dans le cas contraire : blocage de l'interface et affichage du message Application Not Responding (ANR)
Tâches asynchrones (AsyncTask)
- AsyncTask<Params, Progress, Result> : tâche longue à exécuter en fond avec possibilité de mettre à jour incrémentalement l'UI
-
Méthodes à redéfinir :
-
Result doInBackground(Params... params) : code de calcul à exécuter sur la thread secondaire
- Cette méthode doit obligatoirement être redéfinie
- Appels réguliers à publishProgess(Progress... values) pour signaler la progression
- Vérification régulière de isCancelled() pour terminer l'exécution si la tâche est annulée
- onProgressUpdate(Progress... values) : code exécuté sur la thread d'UI pour afficher la progression
- onPreExecute() : code exécuté sur la thread d'UI avant le démarrage du calcul
- onPostExecute(Result r) : code exécuté sur la thread d'UI pour afficher le résultat du calcul
-
Result doInBackground(Params... params) : code de calcul à exécuter sur la thread secondaire
Utilisation de AsyncTask
- Création d'une instance d'AsyncTask avec redéfinition des méthodes spécifiant le code à exécuter pour le calcul et la mise à jour de l'UI
- Appel à execute(Params... params) pour lancer l'exécution
-
Appel éventuel à cancel(boolean interruptIfRunning) pour arrêter la tâche si l'exécution a déjà commencé ou non
- Nécessite de vérifier régulièrement isCancelled() dans le code de calcul
-
Destruction possible de l'activité (changement de configuration, pénurie mémoire...) avant le terme l'AsyncTask
- Première possibilité (simple) : annuler l'AsyncTask dans la méthode onDestroy() de l'activité
- Seconde possibilité (délicate à mettre en œuvre) : laisser persister l'AsyncTask en cas de recréation de l'activité et obtenir une référence vers la nouvelle instance de l'activité
Exemple : calcul de fib(n) avec une tâche asynchrone
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.fibocomputer; import java.lang.ref.WeakReference; import java.math.BigInteger; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.WeakHashMap; import android.app.Activity; import android.os.AsyncTask; import android.os.AsyncTask.Status; import android.os.Bundle; import android.text.method.ScrollingMovementMethod; import android.view.Menu; import android.view.View; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import android.widget.ToggleButton; import fr.upem.coursand.R; public class FiboComputer extends Activity { public static final int PROGRESS_STEP = 10; public class FiboTask extends AsyncTask<Integer, Integer, BigInteger> { private int n = -1; private int activityId; private TextView tv; private ToggleButton tb; private ProgressBar pb; private void updateComponentReferences() { FiboComputer fc = getActivityInstance(activityId); tb = fc.findViewById(R.id.fiboComputeButton); tv = fc.findViewById(R.id.fiboResultTextView); pb = fc.findViewById(R.id.fiboProgressBar); } /** Implentation of the computation, the code is run in a new secondary thread */ @Override protected BigInteger doInBackground(Integer... params) { n = params[0]; BigInteger a = BigInteger.ONE, b = BigInteger.ONE; if (n < 3) return a; for (int i = 3; i < n; i++) { if (isCancelled()) return null; BigInteger tmp = a; a = b; b = tmp.add(a); if (i % PROGRESS_STEP == 0) publishProgress(i); } return b; } /** Called with the result of the computation on the UI thread */ @Override protected void onPostExecute(BigInteger result) { updateComponentReferences(); Toast.makeText(FiboComputer.this, "Fibonacci number has been computed", Toast.LENGTH_LONG).show(); tv.setText(result.toString()); tb.setChecked(false); } /** Called on the UI thread when the computation is stopped */ @Override protected void onCancelled(BigInteger result) { updateComponentReferences(); Toast.makeText(FiboComputer.this, "Fibonacci number computation has been canceled", Toast.LENGTH_LONG).show(); tv.setText("Cancelled"); tb.setChecked(false); } /** We prepare the UI before the computation */ @Override protected void onPreExecute() { activityId = FiboComputer.this.activityId; updateComponentReferences(); Toast.makeText(FiboComputer.this, "Starting the computation", Toast.LENGTH_LONG).show(); tb.setChecked(true); tv.setText(""); } /** We update the progress bar with the reached rank */ @Override protected void onProgressUpdate(Integer... values) { updateComponentReferences(); if (pb.getMax() != n) pb.setMax(n); pb.setProgress(values[values.length - 1]); } } private FiboTask fiboTask = null; /** This map saves all the activity instances (useful for the AsyncTask to track a recreated activity */ private static Map<Integer, WeakReference<FiboComputer>> activityInstances = new HashMap<>(); private static int activityCounter = 0; private int activityId; private FiboComputer getActivityInstance(int id) { return activityInstances.get(id).get(); } private void flushActivityInstances() { // Remove the weak references from the map that are not used anymore Iterator<Map.Entry<Integer, WeakReference<FiboComputer>>> it = activityInstances.entrySet().iterator(); while (it.hasNext()) if (it.next().getValue().get() == null) it.remove(); } public void onComputeButtonClick(View v) { EditText et = FiboComputer.this.findViewById(R.id.fiboQueryEditView); if (fiboTask == null || fiboTask.isCancelled() || fiboTask.getStatus() == Status.FINISHED) { fiboTask = new FiboTask(); fiboTask.execute(Integer.parseInt(et.getText().toString())); } else { fiboTask.cancel(true); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) activityId = savedInstanceState.getInt("activityId"); else activityId = activityCounter++; activityInstances.put(activityId, new WeakReference<>(this)); flushActivityInstances(); setContentView(R.layout.activity_fibo_computer); // to allow the result textview to be scrollable: ((TextView)findViewById(R.id.fiboResultTextView)).setMovementMethod(new ScrollingMovementMethod()); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("activityId", activityId); } }