Skip to main content

One post tagged with "Android performance"

View All Tags

Diagnosing and Mitigating UI Thread Blocking to Prevent ANRs in Kotlin Android Apps

Published: · 5 min read
Sandra Rosa Antony
Software Engineer, Appxiom

Android ANRs surface when the main (UI) thread is blocked long enough that the system stops trusting the process to respond. This post focuses on practical ways to diagnose UI thread blocking and main thread starvation, and how automated monitoring (including Appxiom) can detect UI hangs and reduce ANR rates in production.

What causes ANR in Android: UI thread blocking and main thread starvation

ANR in Android typically occurs when:

  • Input dispatch isn't handled within ~5s (Activity/Window focus and touch).
  • Service starts/operations run too long (~10s).
  • BroadcastReceiver execution exceeds its time budget (~200ms).

Common sources of UI thread blocking in Kotlin Android development:

  • Disk I/O on the main thread (file, SharedPreferences, SQLite).
  • Network calls accidentally executed on Dispatchers.Main or via runBlocking.
  • Heavy JSON parsing, bitmap decoding, or crypto on the main thread.
  • Over-synchronization or long critical sections that block the Looper.
  • Infinite/long animations or tight loops starving the MessageQueue.
  • Excessive work at startup (inflation, reflection, content providers).

Main thread starvation can happen even if no single call is “huge” but the Looper is constantly busy (e.g., tight re-posts to Dispatchers.Main.immediate, hot loops, too many tiny messages).

Kotlin Android development best practices to prevent ANRs

  • Move disk/network/CPU-heavy work off the main thread with coroutines:
class UserRepo(
private val dao: UserDao,
private val api: Api
) {
suspend fun loadUser(userId: String): User = withContext(Dispatchers.IO) {
val local = dao.getUser(userId)
local ?: api.fetchUser(userId).also { dao.insert(it) }
}
}

// UI layer
lifecycleScope.launch {
try {
val user = repo.loadUser("42") // switches to IO for work
render(user) // back on Main
} catch (t: Throwable) {
showError(t)
}
}
  • Prefer Dispatchers.IO for blocking I/O, Dispatchers.Default for CPU work; keep Dispatchers.Main for minimal UI updates.
  • Avoid runBlocking on the main thread; prefer suspend functions and structured concurrency.
  • In Compose, use remember/derivedStateOf wisely; in Views, avoid deep nested layouts and heavy work in onDraw/onLayout.

Enable StrictMode in debug builds to catch disk/network on main:

if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}

Manual debugging: systrace/Perfetto, logs, and ANR traces

  1. Use Perfetto (successor to systrace) to visualize UI thread blocking:
    • Record a trace while reproducing the issue.
    • Inspect Main thread slices, Binder calls, CPU scheduling, and Choreographer frames.
    • Add lightweight markers:
Trace.beginSection("decodeBitmap")
// expensive decode off Main
Trace.endSection()
  1. Inspect ANR traces:

    • Pull from bugreport or /data/anr/traces.txt (requires appropriate permissions).
    • Look for “main” thread stack at the time of ANR; identify blocking call (disk, network, lock).
  2. Log Looper messages and frame jank locally:

    • JankStats (Jetpack) provides frame-dropped info tied to states:
val jankStats = JankStats.createAndTrack(window) { frameData ->
if (frameData.isJank) {
Log.w("Jank", "Jank frame: ${frameData.frameDurationUiNanos} ns, states=${frameData.states}")
}
}
  1. Correlate with app logs:
    • Time-stamp major operations.
    • Log key thread names and durations (e.g., DB queries, JSON parse) to see if they align with jank/ANR windows.

Automated UI hang detection in production

If you only rely on system ANRs, you’ll miss many “near-ANR” stalls that hurt UX. A lightweight watchdog detects UI thread stalls earlier:

class UiBlockDetector(
private val thresholdMs: Long = 700L
) {
@Volatile private var lastBeat = SystemClock.uptimeMillis()
private val handler = Handler(Looper.getMainLooper())

private val ticker = object : Runnable {
override fun run() {
lastBeat = SystemClock.uptimeMillis()
handler.post(this) // Re-post to run again on next loop spin
}
}

fun start() {
handler.post(ticker)
Thread {
while (!Thread.interrupted()) {
val since = SystemClock.uptimeMillis() - lastBeat
if (since > thresholdMs) {
// Main thread stalled; capture state for diagnostics
Log.e("UiBlock", "UI thread stall: ${since}ms")
}
Thread.sleep(thresholdMs / 2)
}
}.start()
}
}

More robust implementations combine:

  • Choreographer frame pacing to catch long frames.
  • Periodic stack sampling of the main thread when a stall is suspected.
  • Correlation with CPU/network/disk metrics to find bottlenecks.

How Appxiom helps reduce ANR rates

In production, Appxiom detects UI thread blocking and main thread starvation by:

  • Observing Choreographer and the main Looper to flag long frames and stalls before they escalate into an ANR in Android.
  • Sampling main-thread stacks during stalls to pinpoint code paths (e.g., disk I/O, network, heavy parsing).
  • Correlating stalls with screen, device, OS version, CPU load, network latency, and DB operations.
  • Aggregating “near-ANR” and ANR events to reveal top offenders and trends over releases.

This automated approach complements manual systrace and logs, making it easier to prevent ANRs at scale.

Quick checklist to prevent ANRs

  • Never block Dispatchers.Main; use withContext(IO/Default) for I/O/CPU.
  • Turn on StrictMode in debug and fix violations.
  • Avoid runBlocking and long synchronized sections on UI paths.
  • Use JankStats to monitor rendering jank; profile with Perfetto for deeper dives.
  • Add Trace sections around expensive code.
  • Deploy a UI stall watchdog in production and track main-thread stacks.
  • Continuously monitor ANR rates and near-ANR stalls; regressions often appear early after releases.

By combining solid Kotlin coroutine practices, manual traces, and automated UI hang detection, teams can reduce ANR rates and deliver a responsive Android experience.