惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
N
Netflix TechBlog - Medium
F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
Y
Y Combinator Blog
博客园 - 聂微东
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog RSS Feed
小众软件
小众软件
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
Jina AI
Jina AI
Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
B
Blog
H
Help Net Security
D
Docker
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
罗磊的独立博客
月光博客
月光博客
博客园 - 司徒正美

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Eliminating Android ANRs in Production
SoftwareDevs · 2026-05-08 · via DEV Community

SoftwareDevs mvpfactory.io

---
title: "Eliminating Android ANRs: From 2.1% to 0.08% ANR Rate"
published: true
description: "A hands-on workshop for fixing the three root causes behind most production ANRs  SharedPreferences traps, binder limits, and broadcast receiver timeouts."
tags: android, kotlin, architecture, mobile
canonical_url: https://blog.mvpfactory.co/eliminating-android-anrs-from-2-1-to-0-08-anr-rate
---

## What We Will Build

In this workshop, we are going to systematically eliminate ANRs from a production Android app. I will walk you through the three root causes responsible for roughly 80% of ANR occurrences in mature codebases, and show you the exact fixes that dropped our ANR rate from 2.1% to 0.08%.

By the end, you will have a custom ANR watchdog, a zero-regression DataStore migration wrapper, a ContentProvider-based payload strategy, and a coroutine-backed BroadcastReceiver pattern you can drop into any project.

## Prerequisites

- A Kotlin-based Android project (minSdk 21+)
- Familiarity with Kotlin coroutines and Jetpack DataStore
- Access to your Play Console vitals (for benchmarking your current ANR rate)

## Step 1: Instrument ANR Detection With a Watchdog

Don't wait for Play Console to tell you about ANRs. Let me show you a pattern I use in every project — a main-thread watchdog that catches ANRs in staging before they reach users:

Enter fullscreen mode Exit fullscreen mode


kotlin
class ANRWatchdog(private val timeoutMs: Long = 5000L) : Thread("ANR-Watchdog") {
private val ticker = AtomicLong(0)

override fun run() {
    while (!isInterrupted) {
        val start = ticker.get()
        Handler(Looper.getMainLooper()).post { ticker.incrementAndGet() }
        sleep(timeoutMs)
        if (ticker.get() == start) {
            reportANR(Looper.getMainLooper().thread.stackTrace)
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

}


This posts to the main looper and checks whether the message was processed within the timeout. If not, it captures the main thread's stack trace. I wish we had added this six months earlier.

## Step 2: Migrate SharedPreferences to DataStore

Here is the gotcha that will save you hours. `SharedPreferences.apply()` is marketed as asynchronous — it is, until `Activity.onPause()` fires. The `ActivityThread` runs `QueuedWork.waitToFinish()` during lifecycle transitions, blocking the main thread until every pending `apply()` completes its disk write.

The docs do not mention this, but every `apply()` call is a latent ANR during lifecycle transitions. Here is the minimal setup to get a safe migration working — a wrapper interface that lets you swap implementations file-by-file without changing call sites:

Enter fullscreen mode Exit fullscreen mode


kotlin
interface KVStore {
suspend fun getString(key: String, default: String = ""): String
suspend fun putString(key: String, value: String)
}

class DataStoreKVStore(
private val dataStore: DataStore
) : KVStore {
override suspend fun getString(key: String, default: String): String =
dataStore.data.map { it[stringPreferencesKey(key)] ?: default }.first()

override suspend fun putString(key: String, value: String) {
    dataStore.edit { it[stringPreferencesKey(key)] = value }
}

Enter fullscreen mode Exit fullscreen mode

}


We migrated 34 SharedPreferences files over three sprints with no regressions using this approach behind a feature flag.

## Step 3: Route Large Payloads Through a ContentProvider

The binder transaction buffer is capped at 1MB per process, shared across all concurrent IPC calls. For payloads exceeding 100KB, stop using Intent extras and pipe through a `ContentProvider`:

Enter fullscreen mode Exit fullscreen mode


kotlin
fun writePayloadToProvider(context: Context, data: ByteArray): Uri {
val uri = PayloadContentProvider.createUri(UUID.randomUUID().toString())
context.contentResolver.openOutputStream(uri)?.use { stream ->
data.inputStream().copyTo(stream, bufferSize = 8192)
}
return uri // Pass this URI in the Intent instead
}


Enforce a 100KB ceiling on Intent extras. A debug-build lint check that logs Bundle sizes above the threshold takes an hour to write and saves weeks of debugging.

## Step 4: Fix BroadcastReceiver Timeouts With goAsync()

BroadcastReceivers get a strict 10-second timeout for foreground broadcasts on the main thread. Any synchronous database query triggers an ANR. Use `goAsync()` paired with coroutines:

Enter fullscreen mode Exit fullscreen mode


kotlin
class SyncReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val pending = goAsync()
CoroutineScope(Dispatchers.IO).launch {
try {
repository.performSync(intent.action)
} finally {
pending.finish()
}
}
}
}


`goAsync()` returns a `PendingResult` that extends the window to 30 seconds and releases the main thread immediately.

## Gotchas

- **`apply()` looks safe but is not.** `commit()` blocks obviously; `apply()` blocks silently at lifecycle transitions. Both are high risk during `onPause()`. DataStore with coroutines carries zero risk.
- **The 1MB binder limit is per-process, not per-call.** Multiple concurrent IPC calls share that buffer. You can hit `TransactionTooLargeException` — or worse, a silent ANR — well below 500KB per individual payload.
- **`goAsync()` buys time, not immunity.** You still get 30 seconds. Always dispatch to `Dispatchers.IO` and keep the work bounded.
- **Enable StrictMode in debug builds.** It flags disk reads/writes and network calls on the main thread. Pair it with the watchdog for full coverage.

## Conclusion

ANRs are not mysterious. They are deterministic — the main thread is blocked for 5+ seconds, and every occurrence has a traceable root cause. Google flags apps above a 0.47% ANR rate, which directly hurts your store ranking.

The audit process: enable StrictMode, deploy the watchdog, grep for `.apply()` and `.commit()`, log Bundle byte sizes, and review every `BroadcastReceiver` subclass. Instrument early, audit systematically, and keep the main thread clear. That is how you go from 2.1% to 0.08%.

Enter fullscreen mode Exit fullscreen mode