¿Cómo mantienes el trabajo pesado fuera del hilo principal en Android — y qué pasa si no lo haces?
El hilo principal tiene un solo trabajo —dibujar el siguiente frame a tiempo— y cada llamada bloqueante que le agregas es una deuda que se cobra como un frame perdido o un ANR.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo mantienes el trabajo pesado fuera del hilo principal en Android — y qué pasa si no lo haces?
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.#Por qué el hilo principal es especial
El hilo principal de Android —también llamado hilo de UI— es donde el framework despacha eventos de entrada, ejecuta measure/layout/draw de las View, y donde los callbacks de ciclo de vida de Activity/Fragment y la mayoría de los callbacks del framework llegan por defecto. Es un único hilo con un Looper sacando mensajes de una cola, uno a la vez, en orden. Nada de eso es lento por sí mismo, pero significa que el hilo solo puede hacer una cosa a la vez: si le entregás una llamada de red, un parseo de JSON grande, una consulta a base de datos o un loop pesado sobre una lista grande, todos los demás mensajes detrás —incluido el trabajo de dibujo del siguiente frame— esperan hasta que esa llamada retorne.
El sistema te da aproximadamente 16ms por frame a 60Hz (o unos 11ms a 90Hz, 8ms a 120Hz —cada vez más común en hardware Android de gama media) para hacer todo el trabajo necesario para presentar el siguiente frame. Si perdés esa ventana, el frame se retrasa —eso es jank—. Si la perdés durante unos 5 segundos mientras se maneja input o un broadcast, el watchdog de Android dispara un diálogo de ANR (Application Not Responding), que es una de las cosas más dañinas que le pueden pasar a la calificación en Play Store porque es el único modo de falla que un usuario no puede confundir con "la app tiene un bug" —se lee como "la app se congeló".
#Qué pertenece realmente fuera del hilo principal
- I/O de disco —lecturas/escrituras de archivos,
SharedPreferences.commit()(nota:apply()es asíncrono en el hilo llamador, pero uncommit()ingenuo en unonClicklo bloquea), y cualquier consulta de Room/SQLite que no esté marcadasuspendo corra vía un executor en background. - Llamadas de red —siempre asíncronas en stacks modernos (Retrofit + coroutines, callbacks de OkHttp), pero fáciles de romper si alguien agrega un
.execute()en lugar de.enqueue()o bloquea sobre unDeferredcon.await()llamado desde el scope equivocado. - Trabajo intensivo de CPU —decodificar/redimensionar imágenes (las operaciones con Bitmap son notorias), parseo de JSON/protobuf de payloads grandes, ordenar o filtrar colecciones grandes, criptografía.
- Cualquier cosa con duración impredecible —una llamada a un SDK de terceros cuya latencia no controlás es un riesgo para el hilo principal incluso si "usualmente" retorna rápido.
#Las herramientas, en concreto
Las coroutines de Kotlin con concurrencia estructurada son el default moderno:
class UserProfileViewModel(private val repository: UserRepository,) : ViewModel() {private val _uiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()fun loadProfile(userId: String) {viewModelScope.launch {_uiState.value = ProfileUiState.Loadingtry {// Dispatchers.IO mueve la llamada bloqueante de red/DB fuera// del hilo principal; withContext suspende acá y retoma en// el dispatcher del llamador (Main) cuando el bloque retorna.val profile = withContext(Dispatchers.IO) {repository.fetchProfile(userId) // llamada suspend de Retrofit + lectura de Room}_uiState.value = ProfileUiState.Loaded(profile)} catch (e: IOException) {_uiState.value = ProfileUiState.Error(e.message ?: "Network error")}}}}
viewModelScope ata la vida de la coroutine a la del ViewModel, así que navegar hacia otra pantalla cancela el trabajo en vuelo automáticamente —sin callback filtrado que actualice una Activity muerta. Dispatchers.IO es un pool de hilos dimensionado para I/O bloqueante; el trabajo ligado a CPU (ordenar listas grandes, transformar imágenes) debería usar Dispatchers.Default en cambio, dimensionado según la cantidad de núcleos.
Para trabajo pesado de CPU fuera de una API amigable con coroutines, WorkManager maneja trabajo diferible en background que debe sobrevivir a la muerte del proceso (subidas, sincronización periódica), mientras que un Executor/HandlerThread plano todavía aparece en código base más antiguo o crítico en performance para hilos de background estrictamente controlados.
#Qué pasa cuando lo hacés mal
// Anti-patrón: red y parseo síncronos directo en onCreateoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_profile)val json = URL("https://api.example.com/profile").readText() // bloquea el hilo principalval profile = Gson().fromJson(json, Profile::class.java) // más trabajo en el hilo principalbinding.nameText.text = profile.name}
Con una conexión rápida esto puede "funcionar" en una demo y aun así estar mal —en una red real con variación de latencia, esto o dispara la NetworkOnMainThreadException de Android (el sistema operativo se protege explícitamente contra este caso específico) o, para trabajo de CPU que no es I/O de red, se come el presupuesto de frame en silencio sin excepción alguna, lo cual es peor porque nada se cae —la app simplemente se traba de forma visible, y el despachador de input eventualmente acumula suficientes eventos de touch sin manejar como para disparar el watchdog de ANR.
#Cómo lo diagnosticás después del hecho
El Profiler de Android Studio (o Perfetto/systrace independientes) muestra la línea de tiempo del hilo principal directamente —barras largas sin interrupción durante un scroll o un tap son la pistola humeante, y StrictMode (StrictMode.setThreadPolicy) se puede configurar en builds de debug para marcar accesos a disco/red en el hilo principal en el momento en que ocurren, convirtiendo un trabazón silencioso en una advertencia ruidosa de Log o un crash durante el desarrollo, mucho antes de que llegue a un dispositivo real en producción.