cswork / libraryOpen practice workspace

Articles

Android ANR Interview Questions: Main-Thread Stalls, Binder Calls, and Trace Evidence

Practice Android ANR interviews with main-thread traces, slow Binder calls, lock owners, late stack dumps, Perfetto evidence, and fix verification.

An Android screen stops responding after a tap. The ANR report's main-thread stack ends in BinderProxy.transactNative, so a candidate says, “Move the binder call to a coroutine.” That might help, but the stack identifies where the main thread waited, not necessarily why the remote reply was late. A strong Android ANR interview answer names the trigger, reconstructs the wait chain across threads or processes, and verifies a measured fix.

Android's official ANR guidance distinguishes input-dispatch, service, broadcast, content-provider and other ANR types. The trace exercises below are original illustrative fixtures, not captured device traces or candidate-reported employer questions. For adjacent practice in Android UI work and cancellation, see Jetpack Compose Auto-Scrolling List with a Start/Stop Toggle and Hex Color Switching.

What makes an ANR different from an ordinary slow frame?

The Android ANR overview describes an application-not-responding event as a missed system responsiveness deadline. For input dispatch, an app that does not respond to an input event within five seconds can trigger an ANR. Other triggers have their own conditions and timeouts; do not apply the five-second input rule to every service or broadcast. The deadline includes the system's view of responsiveness, not a guarantee that exactly one method ran for five seconds.

This distinction matters in an interview. A 200 ms frame is visibly poor UX but is not automatically an ANR. A service-start ANR can occur while an apparently unrelated component monopolizes the main thread before the service callback runs. A content-provider ANR may involve the provider's binder thread or cold start. Android's unresponsive-thread guide maps ANR types to the thread you should inspect first, including cases where it is not the app's main thread.

Ask for the report reason, process, component, OS version, device state, timestamp, affected release, and user action. Those details determine which deadline was missed and which trace belongs to the event. “The UI froze” is a symptom; it is not a thread diagnosis. A candidate should also distinguish app-owned work from a system-wide load event before attributing every slow binder call to their code.

How do you read a main-thread trace without overclaiming?

Start with the ANR type and the unresponsive thread. Then read the top stack frames, thread state, and nearby application frames. A RUNNABLE main thread deep in image decoding or sorting suggests CPU work; a BLOCKED thread waiting to acquire a monitor points to a lock owner; a thread in BinderProxy.transact points to a synchronous cross-process call. These are hypotheses, not complete root causes. The Android trace guide explicitly notes that the actual blocker may be another thread or process.

Use this compact reading order for a supplied report:

EvidenceFirst questionEvidence needed before claiming a cause
ANR reason and componentWhich system deadline fired?Match timestamp and callback; do not reuse a five-second rule blindly.
Main or responsible thread stateRunning, waiting, blocked, or idle?Read stack and scheduler history around the deadline.
BinderProxy.transact*Which API made the call?Follow the reply process, binder thread, load and locks.
BLOCKED on a monitorWho owns this lock?Inspect owner stack and duration, not only waiter stack.
nativePollOnce / looper idleWas the dump captured late?Compare earlier samples, Perfetto timeline and system load.
Repeated clustersIs this one device or release?Group by reason, OS/device, version and user flow.

The nativePollOnce row is an especially good interview test. Android warns that an apparently idle main thread can reflect a late stack dump or system resource constraints, not proof that there was no ANR or that the app was innocent. If the trace is late, the blocking operation may have finished before the stack was captured. Use timeline evidence or additional samples. Likewise, one RUNNABLE snapshot does not tell you whether the thread actually got CPU time during the missed deadline.

Worked trace: the main thread waits on Binder

Assume a search screen triggers an accessibility-related framework call during a user tap. The following synthetic trace fragment is deliberately short; it teaches interpretation, not a specific device signature:

Reason: Input dispatching timed out
main tid=1  Native
  at android.os.BinderProxy.transactNative(...)
  at android.os.BinderProxy.transact(...)
  at example.platform.FeatureProxy.fetchStatus(...)
  at example.app.SearchScreen.onClick(...)

What can you conclude? The tap path synchronously waited for a binder reply on the main thread. You cannot conclude that fetchStatus consumed CPU, that the remote process was dead, or that changing Kotlin syntax fixes the wait. The Android binder-call diagnosis notes that tail latency can grow under device load, remote lock contention, many binder calls, or HAL timeouts. Identify the actual framework or service API below transact, the callee process if trace tooling exposes it, and whether the call is necessary before updating the UI.

Now add a hypothetical Perfetto observation: the app's main thread is sleeping in binder for 4.8 seconds; the remote binder thread waits on a lock held by a worker parsing a large file. The causal chain becomes tap → synchronous call → remote binder thread → lock owner → long parse. The main-thread frame is the blocked caller, while the worker's parse is the immediate bottleneck. This is stronger than “Binder was slow” because it predicts an actionable fix and a before/after trace.

The remedy depends on ownership. If the API can run off the main thread, invoke it through a properly scoped background operation, preserve cancellation and error handling, and render a loading or fallback state. A suspend function alone does not move blocking code off the main thread; the implementation must avoid blocking that dispatcher. If the call must happen on the main thread, reduce its frequency or the remote work, remove lock contention, cache only when correctness allows, and measure again. Do not promise that every framework binder call can be relocated.

What if the trace points to a lock instead?

Try a second original fixture. The main thread shows BLOCKED, waiting for SearchCache; a background refresh thread owns SearchCache while it performs a network request. The temptation is to wrap the UI access in a coroutine. That leaves the lock design unchanged if the coroutine still resumes on the main thread and takes the same monitor. The immediate cause is the worker holding a lock across unpredictable I/O.

Read the owner stack, lock identity, and duration. Move I/O outside the critical section, update shared state under a short lock or immutable snapshot, and publish a state change back to the UI. Then test a slow network and concurrent tap. The fix should reduce main-thread wait without introducing stale reads or a data race. Android's ANR diagnosis lists lock contention among common causes, and its unresponsive-thread guide tells you to inspect the holder.

There are other traps. Main-thread disk reads and heavy computation can block input directly. A deadlock requires both sides of the wait cycle, not just one BLOCKED stack. Many small binder calls in a loop can add up even if none is individually dramatic. A callback that starts work on a worker but immediately calls Future.get() on the main thread is still synchronous from the UI's perspective. An interviewer may hand you only a truncated main-thread stack; the right answer is to ask for the other threads or a trace, not invent their contents.

Which evidence should you collect in a real investigation?

For a local, reproducible event, capture a bug report or trace around the action. The Android ANR overview describes adb bugreport and ANR trace files; on newer releases, multiple /data/anr/anr_* files can exist, and pulling them from a device may require root access. Match the file to the incident time and process. A stack dump after the app has recovered is weaker evidence than a timeline covering the blocked interval.

Perfetto's Android ANR example illustrates why a trace timeline can overturn a stack-only diagnosis: the final stack shows where recording ended, while preceding work may account for the long delay. Inspect main-thread scheduling, runnable versus sleeping time, binder transaction duration, remote thread activity, locks, CPU pressure, and competing app or system work. Name the observation that would falsify your hypothesis. For example, if a supposedly expensive parse finishes before the binder wait begins, it is not the blocker in that occurrence.

For field incidents, start with Play Console or another authorized crash/ANR reporting source if available. Android's ApplicationExitInfo reference documents REASON_ANR and getTraceInputStream() on supported API levels, but the stream can be null or overwritten. It can also be attached to a later exit after an ANR that recovered, so correlate timestamps rather than treating the exit reason as the whole history. Preserve build version, device/OS, event type, and privacy boundaries when collecting diagnostic data.

Use StrictMode in development to catch accidental main-thread disk or network operations, but do not mistake a clean StrictMode run for proof that binder calls, lock waits, startup, or every field condition is safe. A representative release-like build matters: debug instrumentation changes timing. Reproduce under realistic data size, cold and warm starts, slow storage/network, and device load. Collect the same metrics before and after the fix; a single fast emulator run does not close a production ANR cluster.

How would you answer the interview follow-ups?

“Every ANR is a five-second main-thread block, right?” No. Five seconds is the familiar input-dispatch threshold in the official overview; service, broadcast, and content-provider cases have different contracts. Some ANRs involve binder or other responsible threads. First classify the event, then apply its relevant timeout and callback path. Timeouts can also vary by platform version and device implementation, so avoid promising one number for every phone.

“The main thread is idle in nativePollOnce; can we close the bug?” No. The stack may have been captured after the culprit finished, or scheduling/system pressure may be involved. Compare the ANR reason and time with a trace window, other thread states, and device load. If repeated reports show no app-side blocking, say what evidence is missing and how you will gather it. Do not label it a false positive from one frame.

“Why not dispatch everything to Dispatchers.IO?” Moving blocking work away from the main thread can protect input responsiveness, but it does not cure a remote service or a lock holder. An unbounded flood of requests may saturate the background pool and still degrade the app. Define a bounded work policy, cancellation, data freshness and UI fallback. If the application depends on a synchronous framework call, measure its tail latency and redesign the call site or frequency rather than assuming a dispatcher switch changes the remote process.

“What proves the fix?” In a controlled reproduction, the same user action completes while the main thread keeps processing input, and the trace no longer shows the original long wait chain. In production, compare ANR rate for the affected event and cohort across releases, while watching regressions in completion, latency and correctness. An ANR count drop alone may reflect lower traffic or a different device mix; use a comparable denominator and time window. This verification plan is editorial inference from the diagnosis workflow, not a claim of a universal metric threshold.

Sources and further reading