Anatomie et fonctionnement du HandlerThread Android

Le composant HandlerThread encapsule un thread d'arrière-plan avec un mécanisme de boucle de messages (Looper + MessageQueue). Il simplifie l'utilisation d'un Handler pour traiter des tâches sur un thread de travail dédié, en automatisant les étapes de configuration manuelles.

Sans ce composant, la création d'un thread avec une boucle de messages nécessite :

  1. Instanciation et démarrage d'un Thread standard.
  2. Appel explicite à Looper.prepare() dans la méthode run() pour créer une file de messages propre au thread.
  3. Création d'un Handler qui se lie automatiquement au Looper du thread courant.
  4. Invocation de Looper.loop() pour démarrer la boucle, qui bloque le thread en attente de messages.
  5. Appel final à Looper.quit() ou Looper.quitSafely() pour terminer proprement la boucle et libérer le thread, évitant ainsi les fuites de mémoire.

Ce processus est sujet aux erreurs, notamment si une étape d'initialisation ou d'arrêt est omise.

Séquence d'exécution interne

Voici une représentation simplifiée du cycle de vie d'un HandlerThread :

// Implémentation conceptuelle
public class HandlerThread extends Thread {
    private Looper boucleur; // Remplacement de mLooper
    
    public void run() {
        int idThread = Process.myTid();
        
        // 1. Création automatique du Looper
        Looper.prepare();
        
        // 2. Synchronisation pour exposer le Looper
        synchronized (this) {
            boucleur = Looper.myLooper();
            notifyAll(); // Réveil des threads en attente
        }
        
        // 3. Configuration de la priorité
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        
        // 4. Point d'extension (hook)
        onLooperPrepared();
        
        // 5. Boucle de traitement des messages
        Looper.loop();
        
        // 6. Nettoyage
        boucleur = null;
    }
}

Étape 1 : Initialisation du Looper

L'appel à Looper.prepare() initialise la structure de messages pour le thread courant.

// Looper.java
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean autoriseSortie) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Un seul Looper est autorisé par thread");
    }
    // Création d'un nouveau Looper et stockage dans le ThreadLocal
    sThreadLocal.set(new Looper(autoriseSortie));
}

L'objet Looper créé est stocké dans un ThreadLocal, garantissant son unicité par thread.

Étape 2 : Synchronisation et notification

Le bloc synchronized assure que l'attribution du Looper à la varible d'instance est visible par tous les threads et prévient les conditions de course.

public Looper obtenirBoucleur() {
    synchronized (this) {
        // Attente active jusqu'à l'initialisation complète
        while (isAlive() && boucleur == null) {
            try {
                wait(); // Le thread appelant est mis en attente
            } catch (InterruptedException e) {
                // Gestion de l'interruption
            }
        }
        return boucleur;
    }
}

L'utilisation de notifyAll() (plutôt que notify()) réveille tous les threads potentiellement en attente sur cette instance, assurant une initialisation robuste.

Étape 3 : Boucle de messages et traitement

La méthode Looper.loop() constitue le cœur du thread. Elle extrait en continu les messages de la file et les distribue aux Handler associés.

// Looper.java
public static void loop() {
    final Looper moi = myLooper();
    final MessageQueue file = moi.mQueue;
    
    for (;;) {
        // Récupération bloquante du prochain message
        Message msg = file.next();
        if (msg == null) {
            return; // La boucle se termine si le message est nul
        }
        
        // Distribution du message au Handler cible
        msg.target.dispatchMessage(msg);
        
        // Réutilisation du message dans un pool
        msg.recycle();
    }
}

La boucle se termine lorsqu'un appel à file.next() retourne null, ce qui se produit après un appel à quit() ou quitSafely().

Étape 4 : Arrêt du thread

Deux méthodes permettent de terminer le thread de manière contrôlée :

  • quitSafely() : Traite les messages déjà en attente dans la file avant de s'arrêter.
  • quit() : Arrête immédiatement la boucle, en ignorant les messages non traités.
// Interne à MessageQueue
void quitter(boolean proprement) {
    synchronized (this) {
        if (estEnSortie) return;
        estEnSortie = true;
        
        if (proprement) {
            supprimerMessagesFuturs();
        } else {
            viderLaFile();
        }
        // Réveiller le thread du Looper s'il est bloqué sur next()
        nativeWake(ptr);
    }
}

Utilisation pratique

L'usage typique implique trois phases : démarrage, obtention du Handler, et soumission de tâches.

// 1. Création et démarrage
HandlerThread threadWorker = new HandlerThread("MonThreadDeTravail");
threadWorker.start();

// 2. Création d'un Handler lié au Looper du thread
Handler gestionnaire = new Handler(threadWorker.obtenirBoucleur());

// 3. Publication d'une tâche asynchrone
gestionnaire.post(() -> {
    // Code exécuté sur le thread de travail
    Log.d("TAG", "Tâche exécutée en arrière-plan");
});

// 4. Arrêt propre lorsqu'on n'en a plus besoin
gestionnaire.removeCallbacksAndMessages(null); // Optionnel : purge des tâches
threadWorker.quitSafely();

Les tâches publiées via le Handler sont exécutées de manière séquentielle dans l'ordre de leur soumission, ce qui élimine les problèmes de concurrence pour des opérations sur une même ressource.

Comparaison avec un Thread standard

Un Thread classique exécute une tâche unitaire et se termine. Un HandlerThread, à l'instant, reste actif pour traiter une série de messages, agissant comme un service léger pour les tâches asynchrones en arrière-plan. Il évite la surcharge liée à la création et destruction répétée de threads pour des opérations éparses.

Le modèle de conception sous-jacent est la mise en œuvre d'une boucle de messages par thread, fondamentale dans le système de composants Android pour la communication inter-processus et la gestion de l'état.

Étiquettes: Android handler-thread Looper message-queue multithreading

Publié le 6 juillet à 02h22