跪拜 Guibai
← All articles
Android

Handler.post() Doesn't Switch Threads — It Just Drops a Message in the Right Queue

By 爱笑鱼 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Conflating `post()` with thread-switching leads to race conditions, deadlocks, and ANRs when developers assume the calling thread's context carries over. Understanding that Handler is a mailbox, not a dispatcher, makes Android's single-threaded UI model predictable and prevents the common mistake of blocking the main thread's Looper.

Summary

The common shorthand "Handler switches threads" obscures what actually happens. A `post()` call wraps the Runnable into a Message's callback field, stamps the Handler as `msg.target`, and inserts it into the target Looper's MessageQueue. The calling thread's job ends there. The thread bound to that Looper — often the main thread — is already spinning in `Looper.loop()`, where it pulls the Message from the queue and calls `dispatchMessage()`. That dispatch path checks `msg.callback` first, which is why a posted Runnable runs instead of `handleMessage()`.

This mechanism explains why the main thread never exits after `ActivityThread.main()`: it enters `Looper.loop()` and stays there, processing lifecycle events, input, and drawing as messages arrive. A Handler is just a delivery address, not a thread, and it holds a Looper reference, not a direct Thread reference. The Looper was bound to its thread at creation via `ThreadLocal`, but other threads can still obtain that Looper reference and post work to it.

Misunderstandings cascade from treating Handler as an executor. The execution thread is always the Looper's thread, never the thread that called `post()` and never the thread where the Handler object was constructed. Explicitly passing `Looper.getMainLooper()` or a `HandlerThread`'s Looper makes the target unambiguous and avoids the crashes or misrouting that implicit construction can cause.

Takeaways
`handler.post(runnable)` wraps the Runnable into `Message.callback` and enqueues it; the calling thread does not execute the Runnable.
Before enqueuing, `enqueueMessage()` sets `msg.target = this`, so the Handler can be found again when the Message is dequeued.
The target Looper's thread retrieves the Message inside `Looper.loop()` and calls `msg.target.dispatchMessage(msg)`.
`dispatchMessage()` checks `msg.callback` first, so a posted Runnable runs before any `Handler.Callback` or `handleMessage()` override.
A Handler holds a Looper reference, not a Thread; the execution thread is always the one bound to that Looper.
The main thread stays alive because `ActivityThread.main()` ends with `Looper.loop()`, which blocks indefinitely processing messages.
Implicit `new Handler()` fetches the current thread's Looper and crashes if none exists; explicit `new Handler(looper)` is safer.
`ThreadLocal` restricts how a thread queries its own Looper, but does not prevent other threads from holding and posting to that Looper.
Conclusions

The phrase "Handler switches threads" is a pedagogical shortcut that creates more bugs than it prevents, because it hides the queue and the Looper's pull-based execution model.

Many developers never realize that `post()` and `sendMessage()` take different paths through `dispatchMessage()`, which is why a Runnable can silently bypass an overridden `handleMessage()`.

The `msg.target` field is the linchpin of the whole system: it decouples message insertion from dispatch, letting a Message sit in a queue as pure data until the Looper thread retrieves it and routes it back to the originating Handler.

Android's main thread is not special because of Handlers; it is special because `ActivityThread.main()` calls `Looper.prepareMainLooper()` and then enters an infinite loop, making it a message-driven runtime rather than a procedural script.

Concepts & terms
Looper
An Android class that runs a message loop for a thread. It owns a MessageQueue and continuously retrieves Messages via `loop()`, dispatching them to the target Handler. Each thread can have at most one Looper, created by `Looper.prepare()` and stored in a ThreadLocal.
MessageQueue
A priority queue of Message objects ordered by execution time (`when`). It is not a simple FIFO queue; messages with earlier timestamps are delivered first, and the queue can block waiting for the next message's scheduled time.
msg.target
A field on every Message that stores a reference to the Handler that sent it. Set during `enqueueMessage()`, it is used by the Looper thread after dequeuing to call `dispatchMessage()` on the correct Handler instance.
ThreadLocal
A Java class that provides thread-local variables. Android uses a `ThreadLocal<Looper>` so that `Looper.myLooper()` returns only the Looper associated with the calling thread, enforcing the one-Looper-per-thread rule without preventing cross-thread Looper references.
HandlerThread
A convenience class that combines a Thread with a prepared Looper and a running message loop. It is equivalent to manually calling `Looper.prepare()` and `Looper.loop()` on a new thread, providing a background worker that can process posted tasks sequentially.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗