Qué es un ANR y cuándo ocurre

Android monitorea constantemente el Main thread (también llamado UI thread). Si el Main thread no procesa eventos de input durante 5 segundos, el sistema considera que la app está colgada y muestra el diálogo de ANR. Para Services el umbral es 20 segundos; para Broadcast Receivers, 10 segundos.

# Los tres triggers de ANR:

# 1. Input dispatch timeout (el más común)
# → El usuario toca la pantalla y el Main thread no responde en 5 segundos
# → El sistema espera a que el touch sea procesado — si no pasa, ANR

# 2. Service ANR
# → Un Service (started) no termina onCreate() o onStartCommand() en 20 segundos
# → Un Service bound no responde en 20 segundos

# 3. Broadcast ANR
# → Un BroadcastReceiver no termina onReceive() en 10 segundos
# → Muy común con receivers del sistema (boot completed, connectivity changed)

# Lo que NO causa ANR:
# → La app está lenta (frames dropping, UI laggy)
# → Una operación en un background thread tarda mucho
# → El usuario espera una respuesta de red (mientras el UI thread está libre)
# Solo importa si el MAIN THREAD está bloqueado

Cómo leer el ANR trace

Cuando ocurre un ANR, Android genera un archivo de trace en /data/anr/traces.txt (o anr_YYYY-MM-DD.txt en versiones modernas). Play Console también lo incluye en los reportes de ANR. Este archivo contiene el stack trace de todos los threads en el momento del ANR.

# Obtener el trace de un dispositivo conectado:
adb pull /data/anr/traces.txt

# La parte más importante del trace — buscar el Main thread:
# ----- pid 12345 at 2026-04-17 10:30:00.000 -----
# Cmd line: ar.pensa.miapp
#
# DALVIK THREADS (15):
# "main" prio=5 tid=1 Sleeping           ← "main" = Main thread
#   | group="main" sCount=1 dsCount=0 flags=1 obj=0x... self=0x...
#   | sysTid=12345 nice=-10 cgrp=default sched=0/0 handle=0x...
#   | state=S schedstat=( ... ) utm=... stm=...
#   | held mutexes=
#   at java.lang.Thread.sleep (Thread.java)                ← lo que está haciendo
#   at ar.pensa.miapp.MainActivity.onCreate (MainActivity.kt:42)
#   at android.app.Activity.performCreate (Activity.java)
#   ...

# Cómo leerlo:
# 1. Buscar "main" (el Main thread)
# 2. Ver el estado: Sleeping, Blocked, Waiting, Native, Runnable
# 3. Leer el stack trace de arriba hacia abajo para ver qué está ejecutando
# 4. Buscar TU código en el stack trace — ahí está la causa

# Estados del Main thread:
# Sleeping  → Thread.sleep() en el Main thread — siempre es un error
# Blocked   → esperando un lock/mutex que tiene otro thread — posible deadlock
# Waiting   → wait() o join() — el thread espera que otro thread termine
# Native    → ejecutando código nativo (JNI) en el Main thread
# Runnable  → el thread está activo pero el scheduler no le da CPU

Buscar el primer frame de TU código en el stack traceEl trace tiene muchos frames del framework de Android que no podés cambiar. Lo relevante es el primer frame que pertenece a tu paquete (ar.pensa.miapp.*). Ese es exactamente dónde está el problema — la línea de código que bloqueó el Main thread.

Las causas más comunes

# 1. I/O en el Main thread (60% de los ANRs)
# → Acceso a disco: SharedPreferences.commit(), File.readText(), Room sin suspend
# → Acceso a red: HttpURLConnection o Retrofit sin coroutines
# → Base de datos: Room con funciones no-suspend

# 2. Operaciones lentas en el Main thread (20%)
# → Parsear un JSON grande en el Main thread
# → Procesar imágenes o bitmaps sin un thread de background
# → Cálculos intensivos (algoritmos O(n²), sorting de listas grandes)

# 3. Locks y sincronización (10%)
# → Deadlock entre dos threads que se bloquean mutuamente
# → synchronized block que espera un lock retenido por un thread bloqueado
# → Mutex en el Main thread esperando a que un background thread libere

# 4. Binder calls (10%)
# → Llamadas síncronas a servicios del sistema (ContentProvider, PackageManager)
# → IPC (Inter-Process Communication) que tarda más de lo esperado

I/O en el Main thread — la causa más frecuente

// ❌ SharedPreferences.commit() bloquea el Main thread
// (apply() es asíncrono — siempre preferirlo sobre commit())
sharedPrefs.edit()
    .putString("token", token)
    .commit()  // bloquea hasta que el dato se escribe en disco
               // puede tardar decenas de milisegundos

// ✓ Usar apply() que escribe en background
sharedPrefs.edit()
    .putString("token", token)
    .apply()  // asíncrono — retorna inmediatamente

// ❌ Leer un archivo en el Main thread
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val config = File(filesDir, "config.json").readText()  // I/O en Main thread
    procesarConfig(config)
}

// ✓ Leer en un coroutine con Dispatchers.IO
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launch {
        val config = withContext(Dispatchers.IO) {
            File(filesDir, "config.json").readText()
        }
        procesarConfig(config)  // de vuelta en Main thread
    }
}

// ❌ Room sin suspend — bloquea si se llama desde el Main thread
// (Room falla en compilación si lo intentás, pero hay casos donde se cuela)
val productos = dao.getTodos()  // NO suspend → bloquea el thread que lo llama

// ✓ Room con suspend siempre
suspend fun getProductos() = dao.getTodos()  // Room lo mueve automáticamente

// ❌ Bitmap grande en el Main thread
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.imagen_grande)

// ✓ Decodificar en background
lifecycleScope.launch {
    val bitmap = withContext(Dispatchers.Default) {
        BitmapFactory.decodeResource(resources, R.drawable.imagen_grande)
    }
    imageView.setImageBitmap(bitmap)
}

Deadlocks y locks en el Main thread

// Un deadlock clásico — Thread A tiene lock1 y espera lock2,
// Thread B tiene lock2 y espera lock1:

val lock1 = Object()
val lock2 = Object()

// Thread A (Main thread):
synchronized(lock1) {
    Thread.sleep(100)  // simula trabajo
    synchronized(lock2) {  // BLOQUEADO — Thread B tiene lock2
        // nunca llega acá
    }
}

// Thread B (background thread):
synchronized(lock2) {
    Thread.sleep(100)
    synchronized(lock1) {  // BLOQUEADO — Thread A tiene lock1
        // nunca llega acá
    }
}
// Resultado: ambos threads bloqueados para siempre → ANR en el Main thread

// ✓ Cómo evitar deadlocks:
// 1. Siempre adquirir locks en el mismo orden en todos los threads
// 2. Usar coroutines con Mutex en lugar de synchronized:
val mutex = Mutex()

// Thread A:
mutex.withLock { /* trabajo */ }

// Thread B:
mutex.withLock { /* trabajo */ }
// Mutex de coroutines es más seguro — no bloquea el thread, suspende la coroutine

// ❌ Patrón muy común que causa ANR: runBlocking en el Main thread
fun cargarDatos(): List<Dato> {
    return runBlocking {  // bloquea el Main thread hasta que termina
        repository.getDatos()  // puede tardar segundos si hay latencia de red
    }
}

// ✓ Convertir a suspend o usar launch:
suspend fun cargarDatos(): List<Dato> = repository.getDatos()

// O desde un composable/Fragment:
viewModel.cargarDatos()  // el ViewModel usa viewModelScope internamente

runBlocking en el Main thread siempre es una bomba de tiempoEs el error más común de devs que conocen coroutines pero no entienden del todo el modelo de threading. runBlocking bloquea el thread en el que corre — si ese thread es el Main thread, bloquea la UI. Solo es seguro en tests y en funciones main() de scripts.

Binder calls lentas — el ANR menos obvio

Las llamadas IPC a servicios del sistema (ContentProvider, PackageManager, AccountManager) son síncronas y pueden bloquearse si el servicio del sistema está ocupado o el proceso remoto está lento.

// ❌ PackageManager en el Main thread — puede tardar si el sistema está ocupado
val packageInfo = packageManager.getPackageInfo(packageName, 0)

// ❌ ContentResolver en el Main thread
val cursor = contentResolver.query(
    ContactsContract.Contacts.CONTENT_URI, null, null, null, null
)

// ❌ AccountManager síncrono
val accounts = AccountManager.get(context).getAccountsByType("com.google")

// ✓ Todas estas operaciones van en un thread de background:
lifecycleScope.launch {
    val packageInfo = withContext(Dispatchers.IO) {
        packageManager.getPackageInfo(packageName, 0)
    }
    // usar packageInfo en el Main thread
}

// Para ContentProvider — usar CursorLoader o Room que ya maneja el threading
// Para AccountManager — usar las versiones con callback o coroutines

Detectar con StrictMode — antes de que lleguen a producción

StrictMode detecta violaciones de threading en tiempo de desarrollo: te avisa cuando hacés I/O en el Main thread, cuando accedés a la red desde el Main thread, y otros problemas de performance. Activarlo en debug es la forma más efectiva de prevenir ANRs.

// Application.kt — activar en debug
class MiApp : Application() {
    override fun onCreate() {
        super.onCreate()

        if (BuildConfig.DEBUG) {
            // Detectar violaciones de threading
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectAll()          // detectar todo
                    .penaltyLog()         // loggear en Logcat con tag "StrictMode"
                    .penaltyFlashScreen() // parpadeo rojo en pantalla (muy visible)
                    // En tests podés usar penaltyDeath() para que falle con excepción
                    .build()
            )

            // Detectar leaks de memoria y recursos
            StrictMode.setVmPolicy(
                StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()    // cursores sin cerrar
                    .detectLeakedClosableObjects()   // streams sin cerrar
                    .detectActivityLeaks()           // Activities no destruidas
                    .penaltyLog()
                    .build()
            )
        }
    }
}

// Cuando StrictMode detecta una violación, logguea algo como:
// D/StrictMode: StrictMode policy violation; ~duration=127 ms: android.os.StrictMode$StrictModeDiskReadViolation
//   at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1455)
//   at ar.pensa.miapp.MainActivity.cargarConfig(MainActivity.kt:67)
// ↑ Exactamente dónde está el problema

El CPU Profiler — confirmar el diagnóstico

# Android Studio → View → Tool Windows → Profiler
# → Seleccionar el proceso de la app
# → CPU → Record → Callstack Sample

# Para reproducir un ANR en el Profiler:
# 1. Iniciar la grabación
# 2. Realizar la acción que causa el ANR (o la lentitud)
# 3. Detener la grabación
# 4. Buscar el Main thread en el flame graph
# 5. El bloque más ancho = lo que más tiempo toma

# Lo que buscar en el flame graph:
# → Bloques anchos en el Main thread que corresponden a I/O o cálculos
# → "read" / "write" / "connect" en el Main thread
# → Métodos propios que tardan más de ~16ms (un frame) en el Main thread

# System Trace para diagnóstico más preciso:
# Profiler → CPU → System Trace
# Muestra janky frames (frames que tardaron más de 16ms)
# y exactamente qué estaba haciendo el Main thread durante ese tiempo

ANRs en Play Console

# Play Console → Android vitals → ANRs y Crashes
# Muestra:
# → Tasa de ANR: % de sesiones con al menos un ANR
# → ANR rate por versión, dispositivo, país
# → Stack traces agrupados por firma (mismo origen)
# → El trace completo de cada instancia

# Métricas objetivo de Google (para "good app" badge):
# → ANR rate < 0.47% de sesiones diarias
# → Bad behavior threshold: > 8% de sesiones con ANR

# Cómo interpretar un ANR de Play Console:
# 1. Abrir el cluster de ANRs más frecuente
# 2. Buscar el primer frame de TU código en el stack trace
# 3. Esa es la línea exacta que causa el ANR en los dispositivos de usuarios reales
# 4. La información de dispositivo te dice si es específico de algún hardware

# ANRs que no tienen tu código en el stack trace:
# → Pueden ser del sistema operativo o librerías de terceros
# → Revisar si hay una actualización de la librería que lo resuelve
# → Algunos son falsos positivos por lentitud del sistema

Checklist preventivo

# ── EN DESARROLLO ────────────────────────────────────────────
# ✓ StrictMode activado en debug (detectAll + penaltyLog)
# ✓ Ningún acceso a SharedPreferences con .commit() — solo .apply()
# ✓ Ninguna llamada de red en el Main thread
# ✓ Room solo con suspend functions o Flow (no funciones síncronas)
# ✓ Sin runBlocking fuera de tests y funciones main()
# ✓ Bitmaps grandes decodificados con Dispatchers.Default o Coil/Glide

# ── EN CODE REVIEW ───────────────────────────────────────────
# ✓ Buscar Thread.sleep() en código de producción
# ✓ Buscar synchronized{} en el Main thread
# ✓ Buscar withContext sin Dispatchers.IO para operaciones de I/O
# ✓ Verificar que los DAO de Room son suspend o retornan Flow

# ── EN PRODUCCIÓN ────────────────────────────────────────────
# ✓ Monitorear Play Console → Android vitals semanalmente
# ✓ ANR rate target: < 0.47% de sesiones
# ✓ Resolver los 3 clusters de ANR más frecuentes antes de cada release
# ✓ Configurar alertas en Play Console para picos de ANR rate