La guerra contra el background: crónica de un cierre
Durante los primeros años de Android cualquier aplicación podía correr indefinidamente en segundo plano, despertarse ante docenas de sucesos del sistema y mantener el procesador encendido sin rendir cuentas a nadie. Esa libertad produjo un desastre de batería medible y sistemático, y a partir de Android 6 la plataforma empezó una campaña de restricciones que no ha parado desde entonces. Esta lección reconstruye la cronología completa de ese cierre, explica la lógica económica que lo hace inevitable, distingue las tres familias de restricción que operan simultáneamente sobre tu proceso y establece el marco mental con el que hay que leer todo el nivel: el segundo plano dejó de ser un derecho y pasó a ser un permiso condicionado que se concede caso por caso.
Hay una manera perezosa de contar esta historia que consiste en decir que Google endureció las reglas del segundo plano porque los desarrolladores abusaban, y hay una manera exacta que consiste en entender que el modelo original de Android contenía un fallo estructural de incentivos. Cada aplicación individual tenía un motivo legítimo para despertarse un poco más a menudo que las demás: sincronizar antes, notificar primero, tener el dato caliente cuando el usuario abriera la pantalla. Ninguna de esas decisiones era irracional por separado y el conjunto era catastrófico. Un teléfono con cuarenta aplicaciones instaladas acumulaba cientos de despertares por hora, y el usuario percibía el resultado como una única cosa: el teléfono dura poco. La plataforma no podía arreglar eso pidiendo moderación, porque el desarrollador que se moderaba salía perdiendo. Solo podía arreglarlo quitando la capacidad. Y eso es exactamente lo que hizo, versión a versión, durante una década.
- Reconstruir la cronología de las restricciones desde Android 6 hasta las versiones actuales y saber qué cerró cada una.
- Explicar por qué el modelo abierto original era insostenible en términos de incentivos y de física del dispositivo.
- Distinguir las tres familias de restricción que actúan a la vez: aplazamiento, prohibición de arranque y muerte del proceso.
- Reformular tu diseño en términos de trabajo declarado al sistema en lugar de trabajo ejecutado por tu proceso.
El modelo original y por qué se rompió
En el Android de las primeras versiones un Service era, literalmente, código tuyo corriendo sin interfaz durante el tiempo que quisieras. Se arrancaba con startService, devolvía START_STICKY para que el sistema lo resucitara si moría, y desde ahí podía hacer cualquier cosa. En paralelo existía un catálogo generoso de anuncios implícitos del sistema a los que cualquier aplicación podía suscribirse desde el manifiesto: BOOT_COMPLETED para despertar al arrancar el teléfono, CONNECTIVITY_ACTION para reaccionar a cada cambio de red, cambios de batería, montaje de almacenamiento, instalación de paquetes. Suscribirse en el manifiesto significaba que el sistema arrancaba tu proceso solo para entregarte el aviso, aunque el usuario no hubiera abierto tu aplicación en semanas.
La combinación de ambas cosas produce un fenómeno que conviene nombrar con precisión porque explica todo lo demás. Cuando el usuario caminaba con el teléfono en el bolsillo y la red saltaba entre celdas, cada oscilación de conectividad emitía un anuncio que despertaba a las veinte aplicaciones suscritas, cada una arrancando un proceso completo, cargando clases, inicializando su grafo de dependencias y abriendo una conexión para comprobar si había novedades. El coste de esa comprobación no lo pagaba la aplicación que la solicitaba: lo pagaba una batería compartida. Es la estructura exacta de una tragedia de los comunes, con la agravante de que el recurso agotado era invisible y su culpable, indistinguible.
El daño no era solo energético, y esa es la parte que suele olvidarse. Cada proceso resucitado para atender un anuncio ocupa memoria que el sistema tenía asignada a la aplicación que el usuario sí estaba usando, de modo que una ráfaga de anuncios producía una cascada de desalojos y, unos segundos después, aplicaciones que se reiniciaban al volver a ellas. El usuario percibía dos cosas aparentemente inconexas, que la batería duraba poco y que el teléfono iba lento, y ambas tenían el mismo origen.
Hay además una razón física que multiplica el daño y que casi nunca se menciona. El componente de radio de un teléfono no se enciende y se apaga instantáneamente: tras transmitir un solo paquete permanece varios segundos en un estado de alta energía por si llega más tráfico, y luego baja por escalones. Una petición de red minúscula ejecutada cada cinco minutos consume, en la práctica, casi tanta energía como mantener la radio encendida de forma continua. Veinte aplicaciones haciendo peticiones diminutas desincronizadas garantizan que la radio no llegue nunca a dormir. La solución técnica correcta era agrupar, y agrupar exige un árbitro central, porque ninguna aplicación puede coordinarse con las demás.
Conviene añadir un tercer factor que agrava los dos anteriores y que rara vez aparece en las discusiones: la asimetría de información entre quien causa el consumo y quien lo sufre. El usuario que ve su batería agotarse a media tarde no dispone de ninguna herramienta para saber que la culpable fue una aplicación de comercio que consultaba precios cada quince minutos, y la aplicación de comercio tampoco recibe ninguna señal de que su comportamiento está siendo penalizado. En ausencia de retroalimentación, el sistema no puede autorregularse: los buenos ciudadanos no obtienen recompensa y los malos no reciben castigo, de modo que el comportamiento medio deriva sin freno hacia el extremo más agresivo. Las pantallas de consumo por aplicación que Android fue introduciendo son un intento de cerrar ese circuito informativo, pero llegaron tarde y son necesariamente aproximadas.
El coste invisible
Cada despertar arranca un proceso entero, no solo el código que te interesa. El usuario nunca atribuye el consumo a la aplicación correcta.
La radio no duerme
Transmitir un byte mantiene el módem en alta energía durante segundos. Muchas peticiones pequeñas cuestan como una grande y continua.
Incentivos rotos
Moderarse individualmente no mejora la batería del usuario, solo empeora tu producto frente al competidor que no se modera.
La única salida
Retirar la capacidad y sustituirla por interfaces donde el sistema decide cuándo se ejecuta el trabajo de todos a la vez.
La cronología del cierre, versión a versión
La secuencia importa porque cada capa se añadió sobre las anteriores sin retirarlas, y hoy conviven todas simultáneamente. Android 6 introdujo Doze y App Standby: el primero suspende la actividad de red y aplaza las alarmas cuando el dispositivo lleva un rato inmóvil y con la pantalla apagada; el segundo degrada individualmente a las aplicaciones que el usuario no toca. Fue la primera vez que la plataforma decidió por su cuenta no ejecutar algo que una aplicación había pedido.
Android 7 endureció Doze para que se activara también con el dispositivo en movimiento, y eliminó tres anuncios especialmente ruidosos, entre ellos el de cambio de conectividad. Android 8 marcó el punto de inflexión real con dos golpes simultáneos: la prohibición de registrar en el manifiesto la mayoría de anuncios implícitos, y los límites de ejecución en segundo plano, que convierten en ilegal arrancar un servicio ordinario cuando tu aplicación no está en primer plano. A partir de ahí, el Service clásico dejó de ser una herramienta general y pasó a ser un detalle de implementación de otras cosas.
Merece la pena detenerse en Android 8 porque es la frontera que separa dos maneras incompatibles de escribir aplicaciones. Antes de esa versión, el patrón dominante consistía en declarar receptores en el manifiesto y arrancar servicios desde ellos, y prácticamente toda la documentación, los tutoriales y las bibliotecas de la época lo enseñaban así. Después de esa versión ese patrón dejó de funcionar sin que nadie retirara los tutoriales, de modo que durante años convivieron una industria escribiendo código muerto y una plataforma que ya no lo ejecutaba. Buena parte del código heredado que uno se encuentra hoy en proyectos grandes contiene fósiles de esa era: receptores que no reciben nada y servicios que nadie arranca, conservados porque quitarlos parecía arriesgado y nadie comprobó nunca si seguían vivos.
Android 9 añadió los cubos de inactividad, que sustituyeron el interruptor binario de App Standby por una gradación de cinco niveles con cuotas distintas. Android 10 restringió el inicio de actividades desde segundo plano, cerrando la puerta a las aplicaciones que aparecían encima de lo que el usuario estaba haciendo. Android 11 introdujo la revocación automática de permisos por desuso y endureció el acceso a la ubicación en segundo plano. Android 12 prohibió arrancar un servicio en primer plano desde el fondo salvo excepciones tasadas, y exigió permiso explícito para las alarmas exactas.
Android 13 hizo obligatorio el permiso de notificaciones, lo que socavó indirectamente la vieja táctica de justificar cualquier trabajo con una notificación permanente. Android 14 exigió declarar un foregroundServiceType para cada servicio en primer plano, con su permiso correspondiente y un caso de uso documentado. Android 15 impuso un tope temporal a los servicios de tipo sincronización de datos y afinó las condiciones de arranque. La dirección no ha cambiado nunca de signo.
Hay un detalle de mecánica que explica por qué esta cronología no es historia antigua sino un calendario que te afecta ahora mismo. Casi todas estas restricciones se activan en función del targetSdk declarado por tu aplicación, no de la versión del sistema donde se ejecuta. Eso significa que puedes seguir corriendo bajo reglas de hace cinco años en un teléfono moderno mientras no eleves ese valor, y que el día que lo eleves porque la tienda te obliga heredarás de golpe todas las restricciones acumuladas desde tu última actualización. Ese salto es el origen de la mayoría de las crisis de compatibilidad que sufren los equipos que aplazan la actualización: en lugar de absorber un cambio por versión, absorben cinco a la vez y bajo la presión de un plazo administrativo.
android {
defaultConfig {
// Elevarlo activa de golpe todas las restricciones acumuladas
targetSdk = 35
minSdk = 24
}
}
Conviene además distinguir la fecha de publicación de una restricción de la fecha en que empieza a doler. Entre ambas suele mediar el plazo que la tienda concede para elevar el objetivo de compilación, típicamente algo más de un año, y ese margen produce una ilusión de estabilidad peligrosa. Una aplicación puede llevar tres años funcionando sin incidencias y estar acumulando deuda contra restricciones que ya existen, que ya están documentadas y que ya rompen su diseño, simplemente porque todavía no se le aplican. La lectura disciplinada de las notas de versión de cada Android, aunque no vayas a actualizar el objetivo ese año, es la única defensa barata contra ese acantilado.
flowchart TD A[Android 6: Doze y App Standby] --> B[Android 7: Doze en movimiento] B --> C[Android 8: fin de los anuncios implicitos y limites de ejecucion] C --> D[Android 9: cubos de inactividad] D --> E[Android 10: limite al inicio de actividades] E --> F[Android 12: limite al arranque de servicios y alarmas exactas] F --> G[Android 13: permiso de notificaciones] G --> H[Android 14: tipos obligatorios de servicio en primer plano] H --> I[Android 15: topes temporales] style C fill:#f38ba8,color:#11111b style H fill:#f9e2af,color:#11111b
Tres familias de restricción que actúan a la vez
El error de diagnóstico más frecuente al depurar estos problemas es tratar todas las restricciones como una sola cosa. Son tres mecanismos independientes, con causas distintas y síntomas distintos, y confundirlos lleva a soluciones que no arreglan nada.
La primera familia es el aplazamiento. Tu trabajo está permitido, tu código es correcto y el sistema simplemente decide ejecutarlo más tarde, agrupado con el de otras aplicaciones. Aquí viven Doze, los cubos de inactividad y las cuotas de trabajo. El síntoma es una tarea que se completa, pero horas después de lo previsto. No hay excepción ni error, y por eso es la familia que más desconcierta a quien la observa por primera vez en un dispositivo real.
La segunda familia es la prohibición de arranque. Tu proceso está vivo y tiene la intención de iniciar algo, y la plataforma lo rechaza porque no estás en un contexto que lo autorice. Aquí viven la prohibición de servicios en segundo plano y el bloqueo del inicio de actividades. El síntoma es una excepción inmediata y ruidosa, o bien un silencio absoluto en el caso de las actividades, que es peor porque no deja rastro.
Hay un rasgo de esta segunda familia que merece subrayarse porque genera confusión perpetua en los equipos: la misma llamada, con el mismo código y sobre el mismo dispositivo, tiene éxito o fracasa según el estado de importancia que tuviera tu proceso en ese microsegundo. No es indeterminismo, es una condición de contexto que no aparece en ninguna parte del código y que por tanto es invisible al leerlo. Esa es la razón de que los fallos de arranque en segundo plano se comporten como intermitentes y de que la primera reacción de casi todo el mundo sea buscar una condición de carrera que no existe.
La tercera familia es la terminación. El proceso deja de existir por presión de memoria, por exceder un tope temporal o por una política del fabricante más agresiva que la del sistema base. El síntoma es que el trabajo se interrumpe a la mitad y nadie ejecuta ningún callback de despedida. Contra esta familia no hay permiso que valga: la única defensa es que el trabajo sea reanudable e idempotente.
Buena parte de los dispositivos vendidos fuera del ecosistema puro incorporan gestores de energía propios que cierran procesos, revocan alarmas y desactivan trabajos programados con criterios que el fabricante no publica y que a veces exigen que el usuario marque tu aplicación como protegida en un menú escondido. Ninguna cantidad de corrección técnica te inmuniza contra eso. Si tu producto depende de trabajo diferido para funcionar, tienes que probarlo en esos dispositivos concretos y, en los casos graves, guiar al usuario hasta el ajuste. Diseñar contra el comportamiento documentado del sistema base es necesario y no es suficiente.
Cómo se piensa el segundo plano después de la guerra
Antes de entrar en las herramientas concretas conviene fijar el orden de lectura del nivel, porque cada lección responde a una de las familias anteriores. La siguiente aborda el servicio en primer plano, que es la respuesta de la plataforma a la necesidad de trabajo continuo y la única puerta legítima al privilegio pleno. Después llegan Doze y App Standby, que son la familia del aplazamiento y explican por qué el reloj deja de ser fiable. Luego los límites de inicio, que son la familia de la prohibición y la que más código heredado rompe. Y el nivel cierra con el árbol de decisión que reúne todo en una sola pregunta operativa.
La conclusión práctica de toda esta historia es un cambio de vocabulario que arrastra un cambio de arquitectura. Antes se decía ejecutar trabajo en segundo plano y se pensaba en un hilo propio corriendo cuando a uno le convenía. Ahora hay que decir declarar trabajo al sistema y pensar en un contrato: describes qué hay que hacer, bajo qué condiciones y con qué garantías, y el planificador decide el momento. Tú dejas de controlar el cuándo y ganas la única cosa que importa de verdad, que es que el trabajo sobreviva a la muerte de tu proceso y al reinicio del dispositivo.
Ese contrato tiene una propiedad que compensa con creces lo que se pierde y que casi nadie valora hasta que la necesita. Al declarar el trabajo en lugar de ejecutarlo, ganas gratis una serie de garantías que antes había que construir a mano y que casi nadie construía bien: persistencia a través de reinicios, reintento con retroceso exponencial ante fallo de red, condiciones de ejecución expresadas de forma declarativa, unicidad para evitar duplicaciones y encadenamiento de tareas dependientes. El código antiguo que arrancaba un servicio y esperaba lo mejor no tenía ninguna de esas propiedades, y las incidencias que producía eran de las más difíciles de diagnosticar que existen, porque su síntoma era la ausencia de un suceso que debería haber ocurrido.
Hay una prueba mental que resuelve la mayoría de las dudas y que conviene adoptar como reflejo. Ante cualquier operación que quieras ejecutar fuera de la pantalla, imagina que un usuario abre los ajustes de batería, ve el nombre de tu aplicación en la lista de consumo y toca para saber por qué. Escribe la frase que le explicaría ese consumo. Si la frase resulta natural y el usuario asentiría al leerla, la operación es defendible. Si la frase suena a excusa, a jerga técnica o a beneficio para ti antes que para él, el problema no está en cómo lo implementaste sino en que no debería estar ocurriendo así.
De ese cambio se derivan tres reglas de diseño que recorren el resto del nivel. La primera es que si el usuario no está mirando, el trabajo es diferible por defecto y hay que justificar cualquier excepción. La segunda es que toda unidad de trabajo debe poder interrumpirse en cualquier punto y reanudarse sin duplicar efectos, porque va a ser interrumpida. La tercera es que la visibilidad para el usuario y el privilegio de ejecución son la misma moneda: cualquier mecanismo que te permita correr sin restricciones te obliga, a cambio, a mostrar algo que el usuario pueda ver y cancelar.
// Modelo antiguo: el proceso controla el cuando y se lleva el trabajo al morir
startService(Intent(this, SincronizacionService::class.java))
// Modelo actual: se declara un contrato que custodia el sistema
val trabajo = PeriodicWorkRequestBuilder<SincronizarWorker>(6, TimeUnit.HOURS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.build(),
)
.build()
WorkManager.getInstance(contexto)
.enqueueUniquePeriodicWork("sync", ExistingPeriodicWorkPolicy.KEEP, trabajo)
Conviene extraer de esta historia la lección general, porque se repetirá en cada capa de Android que estudies después y en cualquier otra plataforma que te toque habitar. Cuando un ecosistema descubre que el comportamiento individualmente racional de sus participantes produce un resultado colectivamente pésimo, tiene exactamente dos herramientas: pedir moderación o retirar capacidad. La primera nunca funciona, y no por falta de buena voluntad, sino por aritmética: el participante que se modera transfiere una ventaja competitiva al que no lo hace, de modo que la moderación se selecciona en contra de sí misma generación tras generación. La segunda funciona siempre, y su coste es que castiga por igual al abusador y al cuidadoso, lo cual explica la sensación de agravio que acompaña a cada versión nueva de Android entre desarrolladores que jamás hicieron nada indebido. Entender esto cambia la forma de leer las notas de versión. Cada restricción no es una opinión de Google sobre tu código, es la constatación de que existía un canal por el que el coste de una decisión tuya se trasladaba a un recurso compartido sin que nadie pudiera medirlo ni facturarlo. Y de ahí se sigue el corolario predictivo, que es lo verdaderamente útil: si hoy encuentras un mecanismo que te permite consumir batería, red o atención del usuario sin que exista una contrapartida visible y cancelable por él, ese mecanismo va a desaparecer o a encarecerse en alguna versión futura, con independencia de lo bien que lo estés usando tú. Un diseño que descansa sobre ese mecanismo tiene fecha de caducidad aunque hoy funcione perfectamente. La forma madura de construir sobre Android no consiste en dominar las reglas actuales, sino en anticipar hacia dónde apuntan: hacia arquitecturas donde el trabajo se declara en lugar de ejecutarse, donde el privilegio se paga con visibilidad, y donde el usuario conserva en todo momento la capacidad de ver qué corre en su dispositivo y detenerlo.
- Inventaría cada cosa que tu aplicación hace sin que el usuario la esté mirando y clasifícala como diferible, urgente o continua, justificando cada urgencia por escrito.
- Busca en tu manifiesto cada receptor declarado y comprueba cuáles siguen siendo entregables hoy y cuáles quedaron muertos desde Android 8 sin que nadie lo notara.
- Para cada tarea diferible, escribe qué ocurre exactamente si el sistema la ejecuta ocho horas después de lo previsto, y decide si eso es tolerable o si el diseño está mal planteado.
- Toma la operación más larga de tu aplicación y describe el punto exacto en el que sería más dañino que el proceso muriese. Después haz que ese punto deje de ser especial.
- Instala tu aplicación en un dispositivo de un fabricante con gestor de energía propio, déjala sin abrir tres días y comprueba si el trabajo programado sigue ejecutándose.