Analyse du code source du mécanisme de messages Handler sur Android

Le mécanisme Handler est un élément fondamental d'Android, permettant la communication et la commutation entre threads. Il repose sur quatre classes collaboratives : Handler, MessageQueue, Looper et Message.

  • Handler : Classe responsable de l'obtention, de l'envoi et du traitement des messages.
  • MessageQueue : File d'attnete de messages fonctionnant selon le principe premier entré, premier sorti (FIFO).
  • Looper : Gère la boucle infinie de récupération et de distribution des messages depuis la file d'attente.
  • Message : Entité représentant un message, utilisée pour sa distribution et son traitement.

Le principe de fonctionnement central implique que la classe Looper exécute une boucle infinie, extrayant sans cesse des messages de la MessageQueue et les distribuant à un Handler pour traitement.

Pour illustrer l'envoi de données d'un thread secondaire vers le thread principal, considérons un exemple simple avec mise à jour de l'interface utilisateur.


// Définition d'un Handler pour gérer les messages sur le thread principal
Handler gestionnaireUI = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 100:
                Log.i("Exemple", "Message du thread A : " + msg.obj);
                break;
            case 200:
                Log.i("Exemple", "Message du thread B : " + msg.obj);
                break;
        }
    }
};

// Lors d'un clic sur un bouton, démarrer deux threads secondaires
findViewById(R.id.bouton_action).setOnClickListener(v -> {
    new Thread(() -> {
        Message msg = gestionnaireUI.obtainMessage();
        msg.what = 100;
        msg.obj = "Données du premier thread";
        gestionnaireUI.sendMessage(msg);
    }).start();

    new Thread(() -> {
        Message msg = gestionnaireUI.obtainMessage();
        msg.what = 200;
        msg.obj = "Données du second thread";
        gestionnaireUI.sendMessage(msg);
    }).start();
});

Cet exemple démontre comment transférer des informations entre threads en quelques étapes simples. Analysons maintenant le code source du Handler.

Examen du code source de Handler

Le constructeur par défaut de Handler initialise des composants essentiels :


public Handler() {
    this(null, false);
}

public Handler(Callback callback, boolean async) {
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Impossible de créer un Handler dans le thread " + Thread.currentThread()
                    + " sans appel préalable à Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

La méthode Looper.myLooper() récupère le Looper associé au thread courant. Si aucun Looper n'existe, une exception est levée, indiquant que Looper.prepare() doit être appelé au préalable.

L'obtention d'un message via obtainMessage() implique :


public final Message obtainMessage() {
    return Message.obtain(this);
}

public static Message obtain(Handler h) {
    Message m = obtain();
    m.target = h;
    return m;
}

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0;
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

Cette approche utilise le pattern de conception flyweight pour réutiliser les objets Message et optimiser la mémoire. Ensuite, l'envoi d'un message via sendMessage() se propage :


public final boolean sendMessage(Message msg) {
    return sendMessageDelayed(msg, 0);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        Log.e("Handler", "sendMessageAtTime() appelé sans mQueue");
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

La méthode enqueueMessage() insère le message dans la file d'attente en fonction de son temps d'exécution. La file d'attente est une liste chaînée ordonnée, et l'insertion est effectuée en respectant l'ordre chronologique.

Analyse du Looper

Le point d'entrée principal d'une application Android se trouve dans ActivityThread.main() :


public static void main(String[] args) {
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
    Looper.loop();
}

La méthode prepareMainLooper() initialise le Looper pour le thread principal :


public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("Le Looper principal a déjà été préparé.");
        }
        sMainLooper = myLooper();
    }
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Un seul Looper peut être créé par thread.");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

Le Looper est stocké dans un ThreadLocal, garantissant une instance unique par thread. La boucle principale loop() fonctionne comme suit :


public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("Aucun Looper ; Looper.prepare() n'a pas été appelé.");
    }
    final MessageQueue queue = me.mQueue;

    for (;;) {
        Message msg = queue.next();
        if (msg == null) {
            return;
        }
        msg.target.dispatchMessage(msg);
        msg.recycleUnchecked();
    }
}

Cette boucle infinie récupère les messages et les distribue au Handler cible via dispatchMessage(), qui applique une logique de traitement en plusieurs étapes, notamment via des callbacks.

Étiquettes: Android Handler Looper MessageQueue multithreading

Publié le 20 juin à 23h56