跪拜 Guibai
← All articles
Android

Stop Saying 'The Handler Is Stuck': A Stage-by-Stage Troubleshooting Method

By 小明的运行时 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Vague diagnoses like "main thread stuck" waste hours because they skip the evidence that tells you where to fix. This method replaces guesswork with a repeatable chain of proof that works across app and framework threads, and it forces engineers to distinguish delivery delay from execution cost — two problems with entirely different fixes.

Summary

A single symptom like a frozen UI or a slow system_server response can originate from a dozen different points in a Handler message's lifecycle. The wrong conclusion — "the Handler is stuck" — gives no repair direction. The right approach walks the normal main chain (post → enqueue → wait → retrieve → dispatch) and separately checks interruption side paths such as rejected enqueues, removals, lifecycle cancellations, and Looper quits.

Two built-in log signals anchor the investigation. Slow delivery measures the gap between Message.when and the actual dispatch start, pointing to preceding messages, thread scheduling delays, or queue backlogs. Slow dispatch measures the callback's own execution time, pointing to locks, I/O, synchronous Binder calls, or native blocking inside the Runnable. Neither signal is a universal monitor; a missing slow log does not prove the absence of waiting or blocking.

A fixed evidence template replaces vague hand-waving. For every stuck task, state the target message and thread, the post return value, the Message.when, the waiting evidence (delivery log, Perfetto timeline, preceding slice), the execution evidence (dispatch log, thread dump, Binder trace), and a conclusion that names the exact stage and cause while explicitly ruling out alternatives. The method applies equally to app main-thread freezes and system_server service latency, where synchronous Binder calls often chain through multiple processes and the real root cause sits on the peer side.

Takeaways
Always decompose a Handler stall into the five-stage main chain (post, enqueue, wait, retrieve, dispatch) and the interruption side paths (rejection, removal, lifecycle cancel, quit).
Slow delivery means dispatchStart − Message.when exceeded the threshold; investigate preceding messages, thread scheduling, and queue state, not the current callback.
Slow dispatch means dispatchEnd − dispatchStart exceeded the threshold; investigate locks, I/O, synchronous Binder calls, and native blocking inside the current callback.
A post() return value of false means the MessageQueue rejected the enqueue, usually because the Looper is quitting; true only means the queue accepted the request at that moment, not that the message will execute.
nativePollOnce in a thread stack is an observation that no executable message was selected, not a root cause; always back-check enqueue results, when, removals, barriers, and quit state.
Synchronous Binder calls inside a Handler callback can chain latency across processes; the real bottleneck is often on the peer side, not the Handler thread itself.
Write conclusions with a fixed template: target message, target thread, post result, waiting evidence, execution evidence, and a named stage-and-cause while explicitly excluding alternatives.
Conclusions

The most common diagnostic failure is treating Slow delivery and Slow dispatch as interchangeable; they measure completely different intervals and demand opposite investigation directions.

Logging only 'post updateUi' is nearly useless. A log that captures the target Handler, return value, calling thread, and uptime turns a mystery into a checkable fact.

Framework engineers often blame the Handler thread when a thread dump shows it stuck in BinderProxy.transact, but the real culprit is a lock or I/O wait on the peer process that never appears in the local trace.

Setting an overly aggressive Looper slow-log threshold and leaving it on distorts timing measurements because the logging itself becomes a non-trivial source of jitter.

Concepts & terms
Slow delivery
A Looper log that fires when the actual dispatch start time minus the message's scheduled when time exceeds a threshold. It signals that a message began executing late, typically due to a long preceding message, queue backlog, or thread scheduling delay.
Slow dispatch
A Looper log that fires when the dispatch end time minus the dispatch start time exceeds a threshold. It signals that the callback itself (Runnable, Callback, or handleMessage) took too long, pointing to internal locks, I/O, Binder calls, or native blocking.
nativePollOnce
The native method where a MessageQueue thread waits when no message is immediately ready. It is an observed state — the queue is waiting on a timeout, a wake, or registered fd events — not an independent root cause of a stall.
Synchronous barrier (in Handler context)
A mechanism that temporarily prevents normal synchronous messages from being dispatched, used in UI traversal paths. It requires explicit evidence (barrier token, call chain, queue state) to confirm; it is not the default explanation for a late message.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗