Android's MessageQueue Is Not a Queue — It's a Time-Aware Message Selector
Misreading MessageQueue as a plain FIFO leads to wrong assumptions about ordering, delays, and thread idling. Understanding its selection rules — timestamps, barriers, and the native poll/wake boundary — is the prerequisite for diagnosing why a Handler callback didn't fire when expected.
A MessageQueue does more than store messages. It decides which message the Looper can retrieve next by checking each candidate's `when` timestamp, whether a sync barrier is blocking normal messages, and whether an async message can bypass that barrier. When no message is immediately executable, it calculates a timeout and calls nativePollOnce to put the thread into an efficient wait — not a busy spin — until a wake event, fd event, or timeout occurs.
postDelayed does not create a timer thread. It stamps a future `when` on the message and enqueues it; the Looper simply won't select it until that time arrives. A sync barrier is a special message with `target == null` that temporarily blocks synchronous messages behind it, allowing only async messages through — a mechanism ViewRootImpl uses to prioritize frame rendering.
When a message doesn't execute, the root cause is rarely "the queue is blocked." The real checklist starts earlier: was the post accepted, has the Looper quit, was the message removed, or is it simply not yet due? The return value of `handler.post()` — `true` or `false` — is the first diagnostic signal, not an afterthought.
Many developers treat Handler.post as a fire-and-forget call, but the boolean return value is the earliest and most overlooked signal that a message was rejected due to Looper shutdown.
The distinction between Java-layer timeout calculation and native-layer waiting is a clean separation of concerns: the Java side knows message semantics, the native side knows how to sleep efficiently across multiple event sources.
Calling nativePollOnce does not guarantee the thread sleeps; a timeout of 0 causes an immediate return, which is used when IdleHandlers just ran and might have enqueued new work.
Async messages are widely misunderstood as a performance feature; they only change barrier-gating semantics, and converting business Handlers to async can silently reorder execution in unintended ways.