Las solicitudes que tu app realiza a la red son una de las principales causas del agotamiento de la batería, ya que activan las radios móviles o de Wi-Fi que consumen energía. Además de la energía necesaria para enviar y recibir paquetes, estas radios consumen energía adicional solo para encenderse y mantenerse activas. Algo tan simple como una solicitud de red cada 15 segundos puede mantener la radio móvil encendida de forma continua y agotar rápidamente la batería.
Existen tres tipos generales de actualizaciones periódicas:
- Iniciadas por el usuario Realizar una actualización basada en algún comportamiento del usuario, como un gesto de deslizar hacia abajo para actualizar
- Iniciada por la app. Realizar una actualización de forma recurrente
- Iniciada por el servidor. Realizar una actualización en respuesta a una notificación de un servidor
En este tema, se analiza cada uno de estos aspectos y se analizan otras formas de optimizarlos para reducir el agotamiento de la batería.
Optimiza las solicitudes iniciadas por el usuario
Las solicitudes iniciadas por el usuario suelen producirse en respuesta a algún comportamiento del usuario. Por ejemplo, una app que se usa para leer los artículos de noticias más recientes puede permitir que el usuario deslice el dedo hacia abajo para actualizar y buscar artículos nuevos. Puedes usar las siguientes técnicas para responder a las solicitudes iniciadas por el usuario y, al mismo tiempo, optimizar el uso de la red.
Limita las solicitudes de los usuarios
Es posible que quieras ignorar algunas solicitudes iniciadas por el usuario si no son necesarias, como varios gestos de deslizar hacia abajo para actualizar en un período breve para verificar si hay datos nuevos mientras los datos actuales aún están actualizados. Actuar en cada solicitud podría desperdiciar una cantidad significativa de energía, ya que la radio se mantendría activa. Un enfoque más eficiente es limitar las solicitudes iniciadas por el usuario para que solo se pueda realizar una solicitud durante un período, lo que reduce la frecuencia con la que se usa la radio.
Usa una caché
Cuando almacenas en caché los datos de tu app, creas una copia local de la información que tu app necesita consultar. Luego, tu app puede acceder a la misma copia local de la información varias veces sin tener que abrir una conexión de red para realizar solicitudes nuevas.
Debes almacenar en caché los datos de la forma más agresiva posible, incluidos los recursos estáticos y las descargas a pedido, como las imágenes de tamaño completo. Puedes usar encabezados de caché HTTP para asegurarte de que tu estrategia de almacenamiento en caché no haga que tu app muestre datos inactivos. Para obtener más información sobre el almacenamiento en caché de las respuestas de red, consulta Cómo evitar descargas redundantes.
En Android 11 y versiones posteriores, tu app puede usar los mismos conjuntos de datos grandes que otras apps usan para casos de uso, como el aprendizaje automático y la reproducción de contenido multimedia. Cuando la app necesita acceder a un conjunto de datos compartido, primero puede verificar si hay una versión almacenada en caché antes de intentar descargar una copia nueva. Para obtener más información sobre los conjuntos de datos compartidos, consulta Accede a conjuntos de datos compartidos.
Cómo usar mayor ancho de banda para descargar más datos con menos frecuencia
Cuando se establece una conexión a través de una radio inalámbrica, un mayor ancho de banda suele implicar un mayor consumo de batería, lo que significa que la red 5G suele consumir más energía que la red LTE, que, a su vez, consume más que la red 3G.
Esto significa que, si bien el estado de radio subyacente varía según la tecnología de radio, en términos generales, el impacto relativo de la batería en el tiempo de cola del cambio de estado es mayor para las radios de mayor ancho de banda. Para obtener más información sobre el tiempo de cola, consulta La máquina de estado de radio.
Al mismo tiempo, un mayor ancho de banda significa que puedes realizar una carga previa más agresiva, descargando más datos en el mismo tiempo. Tal vez de forma menos intuitiva, debido a que el costo de batería del tiempo de inactividad es relativamente más alto, también es más eficiente mantener la radio activa durante períodos más largos en cada sesión de transferencia para reducir la frecuencia de las actualizaciones.
Por ejemplo, si una radio LTE tiene el doble de ancho de banda y el doble de costo de energía que una 3G, deberías descargar cuatro veces más datos durante cada sesión, o incluso hasta 10 MB. Cuando descargues esta cantidad de datos, es importante que consideres el efecto de la carga previa en el almacenamiento local disponible y que vacíes la caché de carga previa con regularidad.
Puedes usar
ConnectivityManager para registrar
un objeto de escucha para la red predeterminada y
TelephonyManager para registrar
un PhoneStateListener que
determine el tipo de conexión actual del dispositivo. Una vez que se conozca el tipo de conexión, podrás modificar tus rutinas de carga previa según corresponda:
Kotlin
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager private var hasWifi = false private var hasCellular = false private var cellModifier: Float = 1f private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities) hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } } private val phoneStateListener = object : PhoneStateListener() { override fun onPreciseDataConnectionStateChanged( dataConnectionState: PreciseDataConnectionState ) { cellModifier = when (dataConnectionState.networkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1/2f else -> 1f } } private class NetworkState { private var defaultNetwork: Network? = null private var defaultCapabilities: NetworkCapabilities? = null fun setDefaultNetwork(network: Network?, caps: NetworkCapabilities?) = synchronized(this) { defaultNetwork = network defaultCapabilities = caps } val isDefaultNetworkWifi get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_WIFI) ?: false } val isDefaultNetworkCellular get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_CELLULAR) ?: false } val isDefaultNetworkUnmetered get() = synchronized(this) { defaultCapabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false } var cellNetworkType: Int = TelephonyManager.NETWORK_TYPE_UNKNOWN get() = synchronized(this) { field } set(t) = synchronized(this) { field = t } private val cellModifier: Float get() = synchronized(this) { when (cellNetworkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1 / 2f else -> 1f } } val prefetchCacheSize: Int get() = when { isDefaultNetworkWifi -> MAX_PREFETCH_CACHE isDefaultNetworkCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } private val networkState = NetworkState() private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { networkState.setDefaultNetwork(network, networkCapabilities) } override fun onLost(network: Network?) { networkState.setDefaultNetwork(null, null) } } private val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.PreciseDataConnectionStateListener { override fun onPreciseDataConnectionStateChanged(dataConnectionState: PreciseDataConnectionState) { networkState.cellNetworkType = dataConnectionState.networkType } } connectivityManager.registerDefaultNetworkCallback(networkCallback) telephonyManager.registerTelephonyCallback(telephonyCallback) private val prefetchCacheSize: Int get() { return when { hasWifi -> MAX_PREFETCH_CACHE hasCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } }
Java
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); private boolean hasWifi = false; private boolean hasCellular = false; private float cellModifier = 1f; private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onCapabilitiesChanged( @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities); hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI); } }; private PhoneStateListener phoneStateListener = new PhoneStateListener() { @Override public void onPreciseDataConnectionStateChanged( @NonNull PreciseDataConnectionState dataConnectionState ) { switch (dataConnectionState.getNetworkType()) { case (TelephonyManager.NETWORK_TYPE_LTE | TelephonyManager.NETWORK_TYPE_HSPAP): cellModifier = 4; Break; case (TelephonyManager.NETWORK_TYPE_EDGE | TelephonyManager.NETWORK_TYPE_GPRS): cellModifier = 1/2.0f; Break; default: cellModifier = 1; Break; } } }; cm.registerDefaultNetworkCallback(networkCallback); tm.listen( phoneStateListener, PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE ); public int getPrefetchCacheSize() { if (hasWifi) { return MAX_PREFETCH_SIZE; } if (hasCellular) { return (int) (DEFAULT_PREFETCH_SIZE * cellModifier); } return DEFAULT_PREFETCH_SIZE; }
Optimiza las solicitudes iniciadas por la app
Las solicitudes iniciadas por la app suelen ocurrir según un programa, como una app que envía registros o estadísticas a un servicio de backend. Cuando se trata de solicitudes iniciadas por la app, considera la prioridad de esas solicitudes, si se pueden agrupar por lotes y si se pueden aplazar hasta que el dispositivo se esté cargando o esté conectado a una red no medida. Estas solicitudes se pueden optimizar con una programación cuidadosa y con el uso de bibliotecas como WorkManager.
Agrupa las solicitudes de red
En un dispositivo móvil, el proceso de encender la radio, establecer una conexión y mantener la radio activa consume una gran cantidad de energía. Por este motivo, procesar solicitudes individuales en momentos aleatorios puede consumir mucha energía y reducir la duración de la batería. Un enfoque más eficiente es poner en cola un conjunto de solicitudes de red y procesarlas juntas. Esto permite que el sistema pague el costo de energía de encender la radio solo una vez y, aun así, obtener todos los datos que solicita una app.
Usa WorkManager
Puedes usar la biblioteca de WorkManager para realizar el trabajo en un programa eficiente
que considere si se cumplen condiciones específicas, como la disponibilidad de la red
y el estado de la energía. Por ejemplo, supongamos que tienes una subclase Worker llamada DownloadHeadlinesWorker que recupera los titulares de noticias más recientes. Este trabajador se puede programar para que se ejecute cada hora, siempre que el dispositivo esté conectado a una red no medida y la batería del dispositivo no esté baja, con una estrategia de reintento personalizada si hay problemas para recuperar los datos, como se muestra a continuación:
Kotlin
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<DownloadHeadlinesWorker>(1, TimeUnit.HOURS) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build() WorkManager.getInstance(context).enqueue(request)
Java
Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build(); WorkRequest request = new PeriodicWorkRequest.Builder(DownloadHeadlinesWorker.class, 1, TimeUnit.HOURS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build(); WorkManager.getInstance(this).enqueue(request);
Además de WorkManager, la plataforma de Android proporciona varias herramientas más para ayudarte a crear un programa eficiente que complete las tareas de red, como el sondeo. Para obtener más información sobre el uso de estas herramientas, consulta la Guía para el procesamiento en segundo plano.
Optimiza las solicitudes iniciadas por el servidor
Las solicitudes iniciadas por el servidor suelen producirse en respuesta a una notificación de un servidor. Por ejemplo, una app que se usa para leer los artículos de noticias más recientes puede recibir una notificación sobre un lote nuevo de artículos que se ajustan a las preferencias de personalización del usuario, que luego descarga.
Envía actualizaciones del servidor con Firebase Cloud Messaging
Firebase Cloud Messaging (FCM) es un mecanismo liviano que se usa para transmitir datos de un servidor a una instancia de app en particular. Con FCM, tu servidor puede notificar a la app que se ejecuta en un dispositivo específico que hay datos nuevos disponibles para ella.
En comparación con el sondeo, en el que tu app debe hacer ping al servidor con regularidad para consultar datos nuevos, este modelo controlado por eventos permite que tu app cree una conexión nueva solo cuando sabe que hay datos para descargar. El modelo minimiza las conexiones innecesarias y reduce la latencia cuando se actualiza la información dentro de la app.
FCM se implementa utilizando una conexión TCP/IP persistente. Esto minimiza la cantidad de conexiones persistentes y permite que la plataforma optimice el ancho de banda y minimice el impacto asociado en la duración de batería.