Diagnosing Android ANRs Caused by Main-Thread Blocking
A diagnostic workflow for Android ANRs: capture the blocked main-thread stack, classify the cause with a decision table, apply the matching threading or layout fix, and verify on a physical device.
17 Aug 2026, 12:17 UTC

The condition: your app freezes and Android kills it
An Application Not Responding (ANR) dialog appears when the system decides your app's main thread can no longer process input. The classic trigger: the main thread is blocked for roughly 5 seconds while a foreground activity should be responding to touch or key events. Broadcast receivers have their own, separate timeout. If users report "the app just freezes, then Android asks to close it," you are almost certainly looking at main-thread blocking, and the fix is nearly always moving work off that thread — but only after you identify which work it is.
The useful takeaway: don't guess. Capture the blocked stack first, classify the cause, then apply the matching fix. Moving the wrong code to a background thread wastes a release cycle.
Common causes and how to tell them apart
| Cause | What the trace shows | Typical fix |
|---|---|---|
| Synchronous network call on main thread | Frames in Socket, HttpURLConnection, or OkHttp execute() | Move to coroutine/WorkManager; use async APIs |
| Disk I/O on main thread | Frames in FileInputStream, SQLite, or SharedPreferences.apply() storms | Room with suspend functions; batch writes |
| Expensive layout or measure pass | Frames in View.measure/onDraw, deep nesting | Flatten hierarchy, use ConstraintLayout, defer work |
| Lock contention / deadlock | Main thread in BLOCKED or WAITING state on a monitor | Remove shared locks across threads; restructure ownership |
| CPU-bound native (JNI) work | Frames in native methods with no Java frames beneath | Profile native code; run on a worker thread |
| Long-running broadcast receiver | Stack rooted in onReceive() | Delegate to WorkManager; call goAsync() carefully |
Ordered checks
- Reproduce on a physical device. Emulators mask timing issues. Use a mid-range device on Android 10 or later, since ANR behavior and trace collection differ across versions. A UIAutomator script that triggers the suspected flow repeatedly is more reliable than manual tapping.
- Pull the ANR trace. On a reproduced ANR, traces land in
/data/anr/on the device (requires a debuggable build or rooted device to read directly). For production, use Play Console's ANR reports or Firebase Crashlytics, which surface the blocked main-thread stack per cluster. - Capture a live thread trace. In Android Studio, open Profiler > CPU, record a trace while reproducing the freeze, and inspect the main-thread timeline. Look for a single method or call chain dominating the wall-clock time. Note the thread state: RUNNING points to CPU-bound work; BLOCKED/WAITING points to lock contention or synchronous waits.
- Check for layout thrashing. If the trace shows measure/layout frames, open Layout Inspector and count hierarchy depth on the affected screen. Thread dumps alone miss this class of problem.
- If JNI is involved, profile natively. Java-level traces stop at the native boundary. Use a native sampling profiler (such as the simpleperf tool shipped with the NDK) to find the CPU-bound native function before refactoring.
Fixes tied to findings
I/O or network on the main thread
Move the call into a coroutine on an appropriate dispatcher and deliver results back to the UI via observable state:
// In a ViewModel (Kotlin, coroutines)
fun loadReport() {
viewModelScope.launch {
// withContext switches the blocking work off the main thread
val report = withContext(Dispatchers.IO) {
repository.fetchReport() // blocking network + disk work
}
_uiState.value = UiState.Loaded(report) // back on main
}
}Run this in your app module; it requires the kotlinx-coroutines-android dependency and a lifecycle-aware scope such as viewModelScope. Collect the state with StateFlow or LiveData in the fragment so UI updates stay on the main thread. Risk: if fetchReport() is called from a canceled scope mid-write, ensure the repository handles cancellation cleanly.
Deferred or retryable work
For work that doesn't need to block a screen (sync, uploads, cleanup), use WorkManager instead of ad-hoc threads. It survives process death and respects system constraints. A receiver that currently does heavy work in onReceive() should enqueue a OneTimeWorkRequest and return immediately.
Lock contention
If the main thread is BLOCKED on a monitor held by a worker thread (or vice versa), no dispatcher change will help. Identify the shared lock from the trace, then either narrow the critical section or replace shared mutable state with a single-threaded confinement pattern (e.g., all mutations on one dispatcher).
Layout cost
Flatten deeply nested LinearLayout chains into a single ConstraintLayout, move inflation of off-screen views to ViewStub, and avoid triggering requestLayout() inside draw callbacks.
Verifying the fix
Re-run the reproduction scenario on the same physical device. In the CPU profiler, confirm the main thread returns to idle well under the 5-second threshold during the previously freezing operation. Then confirm no new ANR clusters appear in Play Console for the release containing the fix — lab verification alone can miss device-specific contention. Also re-check the screens you touched: moving work off the main thread can surface race conditions (stale UI state, double submissions) that only appear under real timing.
When to escalate
Escalate beyond app-level fixes when: the ANR persists after all main-thread blocking is removed (this can indicate system-service contention, e.g., a slow ContentProvider or binder call to another process); the trace shows the main thread idle but input still not dispatched (possible input-channel or system issue — capture a bug report with adb bugreport); or ANRs cluster on a specific OEM build, which may warrant a device-specific workaround or an issue-tracker report. At that point, a full bug report plus systrace capture gives the platform team what they need.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.