¿Qué causa realmente un ANR en Android, y cómo lo evitás?
Un ANR no es un crash — es el sistema agotando un plazo porque tu hilo principal dejó de responder, y arreglarlo empieza por saber cuál plazo se cumplió.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué causa realmente un ANR en Android, y cómo lo evitás?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.Un ANR — Application Not Responding — no es un crash. No se lanza ninguna excepción. La app sigue viva y el proceso está perfectamente sano; lo que pasa es que el sistema operativo decidió que tu hilo principal dejó de responder dentro de una ventana de tiempo fija, y entonces interviene mostrándole al usuario un diálogo de "esperar / cerrar la app". Entender el mecanismo exacto importa en una entrevista, porque "hacerlo más rápido" no es una respuesta — la pregunta real es "qué hace que Android se rinda con vos, y después de cuánto tiempo".
Los plazos son fijos y conviene saberlos de memoria: unos 5 segundos para el despacho de eventos de entrada (una tecla o un toque que nunca se confirma), unos 10 segundos para que un BroadcastReceiver.onReceive() en primer plano termine (60 segundos si el receiver corre en segundo plano), y alrededor de 20 segundos para que un servicio en primer plano termine sus callbacks de arranque. Distintos subsistemas vigilan relojes distintos, pero todos vigilan lo mismo: si el hilo principal les respondió a tiempo.
Ese "lo mismo" es la causa raíz real. El hilo de UI de Android corre un único Looper sacando mensajes de una sola MessageQueue — eventos de toque y tecla, callbacks de ciclo de vida de Activity y Fragment, callbacks de frame de Choreographer, y la mayoría de las llamadas Binder que llegan desde system_server pasan por ese único hilo, un mensaje a la vez. Bloqueá ese hilo el tiempo suficiente y no solo perdés un frame — retrasás el mismísimo mensaje que le avisaría a algún otro subsistema que seguís vivo. Por eso "no hagas cosas lentas en el hilo principal" es prácticamente toda la disciplina de performance en Android, y el ANR es la violación más dramática posible de esa regla.
En la práctica, las causas se agrupan en una lista corta: llamadas de red o de disco síncronas disparadas directamente desde un click; SharedPreferences.Editor.commit() (que bloquea hasta escribir) usado donde apply() hubiera bastado; una consulta SQLite sin índice recorriendo una tabla grande en el hilo que la invoca; un BroadcastReceiver.onReceive() que hace trabajo real en lugar de despacharlo a otro lado; y —la que sorprende a la mayoría— un deadlock donde un hilo en segundo plano tiene un lock y espera a que el hilo principal libere algo, mientras el hilo principal está bloqueado esperando ese mismo lock. Este último caso produce un ANR con un stack trace del hilo principal que a primera vista parece vacío, porque el hilo no está haciendo trabajo costoso: está simplemente atascado.
Para detectar esto antes de publicar, StrictMode.ThreadPolicy en builds de debug registra (o hace crashear, si sos estricto) el momento exacto en que ocurre I/O de disco o de red en el hilo principal. En producción, Android 11 en adelante expone ApplicationExitInfo, que le permite a tu propia app consultar por qué murió su proceso anterior — incluyendo el trace completo del ANR vía getTraceInputStream() — sin necesitar adb ni un dispositivo rooteado para sacar /data/anr/traces.txt. Los "Android vitals" de Play Console además miden tu tasa de ANR contra un umbral que Google considera "mal comportamiento", que es el número que realmente le llama la atención a un equipo.
Prevenir ANRs es sobre todo disciplina, no trucos ingeniosos: mandá el I/O a Dispatchers.IO desde un scope de corrutinas atado a un ciclo de vida, delegá el trabajo en segundo plano no urgente a WorkManager en lugar de hacerlo inline, mantené onReceive() reducido a despachar trabajo a otro lado (o usá goAsync() con un plazo explícito si de verdad necesitás unos segundos más), y auditá cualquier código que tome más de un lock para asegurar un orden consistente — un orden de locks inconsistente es exactamente cómo el hilo principal termina esperando un lock que no puede conseguir a tiempo.
Abajo está el error clásico y su arreglo. La primera versión bloquea el hilo principal con una llamada síncrona a la base de datos dentro de un listener de click — perfectamente capaz de superar el timeout de 5 segundos de despacho de entrada en un dispositivo lento o con una tabla grande. La segunda versión mueve el mismo trabajo detrás de una corrutina, dejando el hilo principal libre para seguir despachando eventos de entrada mientras la consulta corre.
// MAL: corre en el hilo principal — una tabla grande o un dispositivo// lento pueden superar fácilmente el timeout de 5s de despacho de entrada.button.setOnClickListener {val orders = database.orderDao().loadAllForUserBlocking(userId) // síncronorenderOrders(orders)}
// MEJOR: el hilo principal solo despacha; la consulta corre fuera de él,// y la actualización de UI vuelve automáticamente al cambiar de dispatcher.class OrdersViewModel(private val dao: OrderDao) : ViewModel() {private val _orders = MutableLiveData<List<Order>>()val orders: LiveData<List<Order>> = _ordersfun loadOrders(userId: String) {viewModelScope.launch {val result = withContext(Dispatchers.IO) {dao.loadAllForUserBlocking(userId)}_orders.value = result // vuelve a Main automáticamente}}}
Una respuesta senior no se queda en "movelo fuera del hilo principal". También separa los tres modos de falla que desde afuera parecen todos un ANR — una llamada síncrona genuinamente lenta, un deadlock (hilo principal bloqueado en un lock, sin quemar CPU), y una pausa larga de garbage collection causada por exceso de allocations (hilo principal bloqueado, pero por el runtime, no por tu código) — porque el arreglo y el trace se ven distintos en cada caso, y leer una espera de lock como "código lento" es una forma común de publicar un fix que no arregla nada.
🧠 ANR: poné a prueba lo que entendiste
0/3 · 0/3 answered1. Un ANR se dispara cuando...
2. ¿Por qué un deadlock entre un hilo en segundo plano y el hilo principal suele mostrar un trace donde el hilo principal parece inactivo, no ocupado?
3. ¿Cuál es la ventaja práctica principal de ApplicationExitInfo frente a sacar /data/anr/traces.txt a mano?