Congelación (bloqueo) es un estado en el que una aplicación móvil deja de responder a cualquier acción del usuario durante un período prolongado. A diferencia de los lags (ralentización) y los glitches (comportamiento incorrecto), la congelación bloquea completamente la UI: los toques no se procesan, la animación se detiene, la pantalla se “congela”. La causa es el bloqueo del hilo principal por una operación síncrona, un deadlock en código multihilo o una recolección de basura anormalmente larga. Según la Documentación de Apple Main Thread Checker, más del 40% de los informes de crash en iOS están relacionados con el bloqueo del hilo principal. En Android, una situación similar provoca ANR, el diálogo del sistema “La aplicación no responde”.
Puntos clave
Congelación (bloqueo) en una aplicación móvil es un estado en el que la aplicación deja de procesar eventos de entrada y actualizar la interfaz durante varios segundos o más. Técnicamente, esto significa que el hilo principal está bloqueado y no puede ejecutar la siguiente iteración del bucle de ejecución.
Un lag es un retraso de hasta 500 ms en el que el usuario nota una ralentización pero la aplicación sigue funcionando. Congelación dura desde 1 segundo hasta decenas de segundos. ANR en Android es un caso especial de congelación que duró más de 5 segundos y fue detectado por el sistema. No toda congelación lleva a ANR, pero todo ANR es una congelación documentada por el sistema.
En Android, una congelación de más de 5 segundos provoca un diálogo ANR que ofrece cerrar la aplicación. En iOS, el sistema tiene un watchdog: si la aplicación no responde a eventos durante 10–20 segundos, el Watchdog termina el proceso con el código 0x8badf00d (ate bad food). El usuario solo ve que la aplicación se cierra repentinamente a la pantalla de inicio.
Cualquier operación que tarde más de 100 ms y se ejecute en el hilo principal puede causar potencialmente una congelación. Veamos las principales fuentes de bloqueos.
Leer un archivo grande, una solicitud de red sin asincronía, guardar datos en SharedPreferences con el método síncrono apply seguido de commit — todas estas operaciones bloquean el hilo principal. En Android, leer sincrónicamente un archivo de 10 MB puede tomar 200–500 ms dependiendo de la velocidad de la memoria flash. En iOS, una carga síncrona de URLSession sin completionHandler bloquea la UI durante el tiempo de respuesta del servidor.
Cuando dos hilos esperan recursos retenidos mutuamente, se produce un deadlock. En aplicaciones móviles, un escenario típico es que el hilo A bloquea Lock1 y espera Lock2, mientras que el hilo B bloquea Lock2 y espera Lock1. Ambos hilos se congelan para siempre. Si uno de ellos es el hilo principal, la aplicación se congela completamente.
Un error de lógica — por ejemplo, while(true) sin condición de salida o recursión sin caso base — provoca una ejecución infinita en el hilo principal. Android detecta esto mediante ANR después de 5 segundos, iOS — mediante Stackshot, que captura una pila de llamadas que se repite infinitamente.
El diagnóstico de congelaciones requiere herramientas capaces de capturar el estado de todos los hilos en el momento del bloqueo.
En cada ANR, el sistema Android guarda un archivo /data/anr/traces.txt que contiene un volcado de pila de cada hilo de la aplicación. Analizar este archivo es el método principal de diagnóstico: hay que encontrar el hilo main y ver en qué método se detuvo. Si la pila termina en Thread.sleep, InputStream.read o Lock.lock — la causa está encontrada.
Xcode puede tomar un Stackshot — una instantánea de las pilas de todos los hilos — cuando la aplicación se congela (señal SIGSTOP). Active “Logging” → “Include Stackshot Logs” en el esquema. En un crash con código 0x8badf00d, extraiga el registro de crash de Devices & Simulators y busque el hilo com.apple.main-thread con la pila bloqueada.
Main Thread Checker detecta automáticamente llamadas a UIKit desde hilos en segundo plano mientras la aplicación se ejecuta. Actívalo en el esquema (Diagnostics → Main Thread Checker). Cada advertencia es una causa potencial de congelación, especialmente si ocurre en un closure completionHandler de una solicitud de red.
Ejemplo de detección de bloqueos mediante StrictMode en Android:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
Eliminar las congelaciones comienza con mover todas las operaciones potencialmente largas a hilos en segundo plano. Veamos técnicas específicas para cada plataforma.
Kotlin Coroutines con viewModelScope.launch(Dispatchers.IO) garantizan que las operaciones de red o lecturas de base de datos se ejecuten en un hilo en segundo plano. Dispatchers.Main se usa solo para actualizar la UI. Importante: todas las funciones suspend deben estar estructuradas — las corrutinas hijas se cancelan cuando se cancela la padre, evitando fugas de hilos.
Grand Central Dispatch con DispatchQueue.global(qos: .userInitiated) para tareas en segundo plano y DispatchQueue.main.async para actualizaciones de UI es el patrón estándar. Evite sync() en la cola principal — esto es un deadlock garantizado. Use async/await (Swift 5.5+) para código asíncrono más legible con retorno automático al hilo principal mediante MainActor.
Los bloques synchronized en Kotlin y @synchronized en Swift en el hilo principal son peligrosos: si otro hilo ya ha adquirido este bloqueo, el hilo principal se congelará esperando. Use tipos atómicos (AtomicInteger, propiedades atómicas en Swift) o colas seriales en lugar de bloqueos.
Ejemplo de carga asíncrona de datos con corrutinas en Android:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
Una combinación de herramientas, principios arquitectónicos y procesos de revisión de código ayuda a prevenir sistemáticamente las congelaciones.
Configure StrictMode con penaltyDeath para las políticas de hilos — esto provocará un cierre inmediato de la aplicación cuando se detecte una llamada de red o E/S de disco en el hilo principal. El desarrollador no podrá ignorar el problema. En compilaciones de producción, use penaltyLog para recopilar estadísticas sin cierres.
En iOS, active Main Thread Checker en el esquema Debug y configure CI para ejecutar pruebas con esta opción. Si una prueba contiene una llamada a UIKit desde un hilo en segundo plano — debe fallar. Esta es la única forma fiable de identificar el problema antes de enviar a TestFlight.
Agregue un elemento obligatorio al proceso de revisión de código: verificar que cualquier llamada de red, operación con archivos, acceso a base de datos o cálculo pesado se ejecute en un hilo en segundo plano. Deadlock se puede detectar con analizadores estáticos: Infer de Facebook y Thread Safety Checker de Xcode encuentran bloqueos potenciales antes de la ejecución.
Preguntas frecuentes
ANR (Application Not Responding) es una notificación del sistema Android que aparece cuando el hilo principal se congela durante más de 5 segundos. La congelación es un concepto más amplio: cualquier bloqueo de UI de cualquier duración. iOS no tiene ANR, pero tiene un Watchdog con un tiempo de espera de 10–20 segundos.
El archivo se encuentra en /data/anr/traces.txt. El acceso requiere root o adb shell: ejecute adb shell cat /data/anr/traces.txt \> traces.txt con privilegios de root. En la pila, busque el hilo “main” — el último método llamado indica la causa del bloqueo.
Si la congelación dura menos de 10 segundos, el Watchdog no se activa y la aplicación simplemente se “bloquea” hasta que se completa la operación bloqueante. El usuario no ve un crash, pero experimenta frustración. Para detectar estos casos, use MetricKit con trazas personalizadas de tiempo de ejecución.
Use pruebas de UI verificando que la pantalla se abra en < 1 segundo. Agregue a CI la medición del tiempo entre el toque y la aparición de la siguiente pantalla. En Android, use Espresso con IdlingResource para esperar operaciones asíncronas. En iOS, use XCTest con XCTWaiter para verificar el tiempo de carga.
SwiftUI por sí mismo no causa congelaciones, pero los cálculos complejos en la propiedad body sí. Si body tarda 500 ms en calcularse debido a operaciones pesadas, la UI se congela. La solución es descargar los cálculos a Task.detached y actualizar @State asíncronamente en el actor principal.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también