Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Cómo optimizar las descargas para lograr un acceso de red eficiente

El uso de la radio inalámbrica para transferir datos es quizás una de las fuentes más importantes de consumo de la batería de tu app. Con el fin de minimizar el consumo de la batería asociado con la actividad de red, es esencial que comprendas cómo el modelo de conectividad afectará al hardware subyacente de la radio.

En esta lección, se presenta la máquina de estado de la radio inalámbrica y se explica cómo el modelo de conectividad de tu app interactúa con ella. A continuación, se proponen formas de minimizar las conexiones de datos, usar la precarga y agrupar las transferencias para minimizar el consumo de la batería asociado con las transferencias de datos.

Máquina de estado de radio

Una radio inalámbrica totalmente activa consume una gran cantidad de energía, por lo que transita entre diferentes estados con el fin de conservar la energía cuando no está en uso, mientras se intenta minimizar la latencia asociada con el "encendido" de la radio cuando es necesario.

La máquina de estado de una radio de red 3G típica consta de tres estados de energía:

  1. Energía completa: Se utiliza cuando una conexión está activa, lo que permite que el dispositivo transfiera datos a la velocidad máxima posible.
  2. Energía baja: Se trata de un estado intermedio que usa aproximadamente el 50% de la energía de la batería en estado completo.
  3. En espera: Es el estado de energía mínimo durante el cual no se requiere ni se activa ninguna conexión de red.

Si bien los estados "energía baja" y "en espera" consumen mucha menos batería, también presentan una latencia significativa a las solicitudes de red. Regresar al estado de energía completa desde el estado de energía baja lleva aproximadamente 1.5 segundos, mientras que pasar del modo de espera al estado de energía completa puede llevar más de 2 segundos.

Para minimizar la latencia, la máquina de estado usa un retraso para posponer la transición a estados de energía más baja. En la figura 1, se usan los tiempos de AT&T para una típica radio 3G.

Figura 1: Máquina de estado de radio inalámbrica 3G convencional

La máquina de estado de radio en cada dispositivo, particularmente el retraso de transición asociado ("tiempo de cola") y la latencia de inicio, variarán según la tecnología de radio inalámbrica empleada (2G, 3G, LTE, etc.), y está definida y determinada por la red del operador a través de la cual opera el dispositivo.

En esta lección, se describe una máquina de estado representativa para una radio inalámbrica 3G típica basada en datos proporcionados por AT&T. Sin embargo, los principios generales y las mejores prácticas resultantes se aplican a todas las implementaciones de radio inalámbrica.

Este enfoque es muy eficaz para la navegación web típica, ya que evita la latencia no deseada mientras los usuarios navegan por la Web. El tiempo de cola relativamente bajo también garantiza que una vez que haya finalizado la sesión de navegación, la radio podrá pasar a un estado de menor energía.

Lamentablemente, este enfoque puede conducir a apps ineficaces en los SO de teléfonos inteligentes modernos como Android, donde las apps se ejecutan tanto en primer plano (donde la latencia es importante) como en segundo plano (donde se debe priorizar la duración de la batería).

Cómo las apps tienen un impacto en la máquina de estado de radio

Cada vez que creas una nueva conexión de red, la radio pasa al estado de energía completa. En el caso de la típica máquina de estado de radio 3G que se describe arriba, permanecerá con energía completa durante la duración de la transferencia, más 5 segundos adicionales de tiempo de cola, seguidos de 12 segundos en el estado de energía baja. Entonces, para un dispositivo 3G típico, cada sesión de transferencia de datos hará que la radio tome energía durante casi 20 segundos.

En la práctica, esto significa que una app que transfiere datos sin empaquetar durante 1 segundo, cada 18 segundos, mantendrá la radio inalámbrica siempre activa y la moverá nuevamente al estado de energía alta justo cuando estaba a punto de entrar en modo de espera. Como resultado, cada minuto consumirá batería en el estado de alta potencia durante 18 segundos y en el estado de energía baja durante los 42 segundos restantes.

En comparación, la misma app que empaqueta transferencias de 3 segundos por minuto mantendrá la radio en estado de energía alta durante solo 8 segundos y en estado de energía baja durante solo 12 segundos más.

En el segundo ejemplo, se permite que la radio esté en espera durante 40 segundos adicionales cada minuto, lo que resulta en una reducción masiva en el consumo de la batería.

Figura 2: Uso de energía de radio inalámbrica relativa para transferencias empaquetadas y sin empaquetar

Datos de precarga

La precarga de datos es una forma eficaz de reducir el número de sesiones de transferencia de datos independientes. La precarga permite descargar todos los datos que probablemente necesites durante un período determinado en un solo pico de actividad, a través de una única conexión, con capacidad completa.

Mediante la carga de las transferencias, se reduce la cantidad de activaciones de radio necesarias para descargar los datos. Como resultado, no solo conserva la vida útil de la batería, sino que también se mejora la latencia, se reduce el ancho de banda requerido y se reducen los tiempos de descarga.

La precarga también proporciona una experiencia del usuario mejorada, ya que minimiza la latencia en la app causada por la espera a que se completen las descargas antes de realizar una acción o mostrar datos.

Sin embargo, si se usa demasiado, la precarga presenta el riesgo de aumentar el consumo de la batería y el ancho de banda, así como la cuota de descarga, ya que se descargan datos que no se usan. También es importante garantizar que la precarga no retrase el inicio de la app mientras la aplicación espera a que se complete. En términos prácticos, puede significar que se procesen datos de forma progresiva o que se inicien transferencias consecutivas con prioridad, de modo que se descarguen y se procesen primero los datos necesarios para el inicio de la app.

La intensidad de la precarga depende del tamaño de los datos que se descargan y de la probabilidad de que se usen. Como guía general, según la máquina de estado que se describe arriba, los datos que tienen un 50% de probabilidades de usarse en la sesión actual de usuario, en general, se pueden precargar durante aproximadamente 6 segundos (de 1 a 2 Mb) antes de que el costo potencial de la descarga de datos sin usar coincida con los ahorros potenciales de no descargar los datos.

En términos generales, te recomendamos precargar los datos, de modo que solo tengas que iniciar otra descarga cada 2 a 5 minutos, en el orden de 1 a 5 megabytes.

Según este principio, las descargas grandes, como los archivos de video, se deben descargar en fragmentos a intervalos regulares (cada 2 a 5 minutos), y se deben precargar de manera eficaz solo los datos de video que posiblemente se verán en los próximos minutos.

Ten en cuenta que se deben empaquetar más descargas, como se describe en la siguiente sección, Transferencias y conexiones por lotes, y que estas aproximaciones variarán según el tipo de conexión y la velocidad, como se describe en Cómo modificar los patrones de descarga en función del tipo de conectividad.

Estos son algunos ejemplos prácticos:

Reproductor de música

Puedes optar por precargar un álbum completo. Sin embargo, si el usuario deja de escuchar música después de la primera canción, se perderá una gran cantidad de ancho de banda y batería.

Un mejor enfoque sería mantener en el búfer una canción más, además de la que se está reproduciendo. Para transmitir música, en lugar de mantener una transmisión continua que mantenga activa la radio en todo momento, procura usar la transmisión HTTP en vivo para transmitir el audio en picos de actividad, simulando el enfoque de precarga descrito anteriormente.

Lector de noticias

Muchas apps de noticias intentan reducir el ancho de banda mediante la descarga de titulares solo después de que se haya seleccionado una categoría, de artículos completos solo cuando el usuario quiera leerlos y miniaturas justo cuando se desplaza la vista.

Con este enfoque, la radio se verá obligada a permanecer activa durante la sesión de lectura de noticias de la mayoría de los usuarios a medida que se desplazan por los titulares, cambian las categorías y leen artículos. No solo eso, sino que el cambio constante entre estados de energía dará como resultado una latencia significativa al cambiar de categoría o leer artículos.

Un mejor enfoque sería precargar una cantidad razonable de datos al inicio, comenzando con el primer conjunto de titulares y miniaturas de noticias, lo que asegura un tiempo de inicio de baja latencia, y continuando con los titulares y las miniaturas restantes, así como con el texto de cada artículo disponible, al menos, en la lista principal de titulares.

Otra alternativa es precargar cada titular, miniatura, texto del artículo y posiblemente incluso imágenes completas del artículo, en general, en segundo plano, en un horario predeterminado. Con este enfoque, se corre el riesgo de dedicar una gran cantidad de ancho de banda y duración de la batería a la descarga de contenido que nunca se usará, por lo que debe implementarse con precaución.

Una solución es programar la descarga completa para que se produzca solo cuando haya conexión a Wi-Fi, y posiblemente solo cuando el dispositivo se esté cargando. Esto se investiga con más detalles en Modifica los patrones de descarga en función del tipo de conectividad.

Transferencias y conexiones por lotes

Cada vez que inicias una conexión, cualquiera sea el tamaño de la transferencia de datos asociada, puedes hacer que la radio consuma energía durante aproximadamente 20 segundos usando una radio inalámbrica 3G típica.

Una app que hace ping en el servidor cada 20 segundos, solo para reconocer que la app se está ejecutando y es visible para el usuario, mantendrá la radio encendida indefinidamente, lo que resultará en un costo de batería significativo y casi ninguna transferencia de datos real.

Con eso en mente, es importante empaquetar las transferencias de datos y crear una cola de transferencia pendiente. Si lo haces de forma correcta, puedes modificar con eficacia las transferencias de cambio de fase que se producen en una ventana de tiempo similar, para que todo suceda simultáneamente, y asegurar que la radio consuma energía durante el menor tiempo posible.

La filosofía subyacente de este enfoque es transferir la mayor cantidad de datos posible durante cada sesión de transferencia, en un esfuerzo por limitar la cantidad de sesiones que necesitas.

Esto significa que debes agrupar en lote tus transferencias colocando en cola las transferencias con tolerancia a retrasos y adelantar temporalmente las actualizaciones programadas y las precargas, de modo que se ejecuten cuando se requieran transferencias urgentes. Del mismo modo, las actualizaciones programadas y la precarga regular deben iniciar la ejecución de la cola de transferencias pendientes.

Para ver ejemplo práctico, volvamos a los ejemplos anteriores de datos precargados.

Toma una aplicación de noticias que use la rutina de precarga que se describe arriba. El lector de noticias recopila información analítica para comprender los patrones de lectura de sus usuarios y clasificar las historias más populares. Para mantener las noticias actualizadas, busca actualizaciones cada una hora. Para conservar el ancho de banda, en lugar de descargar fotos completas para cada artículo, solo precarga miniaturas y descarga las fotos completas cuando se seleccionan.

En este ejemplo, se debe empaquetar toda la información analítica recopilada dentro de la app y colocarse en cola para descarga, en lugar de transmitirse a medida que se recopila. El paquete resultante se debe transferir cuando se descarga una foto en tamaño completo o cuando se realiza una actualización por hora.

Cualquier transferencia urgente u on demand, como la descarga de una imagen en tamaño completo, debe evitar las actualizaciones programadas regularmente. La actualización planificada se debe ejecutar al mismo tiempo que la transferencia on demand, y la próxima actualización se programará después del intervalo establecido. Este enfoque disminuye el costo de realizar una actualización regular aprovechando la descarga necesaria de las fotografías más urgentes.

Reduce las conexiones

En general, es más eficiente reutilizar las conexiones de red existentes que iniciar otras nuevas. La reutilización de las conexiones también permite que la red reaccione de manera más inteligente ante la congestión y los problemas de datos de red relacionados.

En lugar de crear múltiples conexiones simultáneas para descargar datos o encadenar múltiples solicitudes de GET consecutivas, siempre que sea posible, debes empaquetar esas solicitudes en un único GET.

Por ejemplo, sería más eficiente hacer una sola solicitud para que cada artículo de noticias se muestre en una única solicitud/respuesta, en lugar de hacer múltiples consultas para varias categorías de noticias. La radio inalámbrica se debe activar para transmitir los paquetes de rescisión/confirmación de rescisión asociados con el tiempo de espera del servidor y el cliente, por lo que también es una buena práctica cerrar las conexiones cuando no están en uso, en lugar de respetar estos tiempos de espera.

Dicho esto, cerrar una conexión demasiado pronto puede evitar que se reutilice, lo que luego requerirá una sobrecarga adicional para establecer una nueva conexión. Un método útil es no cerrar la conexión de inmediato, sino hacerlo antes de que caduque el tiempo de espera inherente.

Identifica problemas con el generador de perfiles de red

Usa el generador de perfiles de red para hacer un seguimiento cuando la aplicación realiza solicitudes de red. Puedes supervisar cómo y cuándo la app transfiere datos y optimiza el código subyacente de manera adecuada.

En la Figura 3, se muestra un patrón de transferencia de pequeñas cantidades de datos con aproximadamente 15 segundos de diferencia, lo que sugiere que la eficacia podría mejorarse de forma considerable mediante la precarga de cada solicitud o empaquetando las cargas.

Figura 3: Uso de la red de seguimiento

Si supervisas la frecuencia de las transferencias de datos y la cantidad de datos transferidos durante cada conexión, podrás identificar las áreas de la aplicación en las que se puede hacer un uso más eficaz de la batería. Por lo general, buscarás picos cortos que se pueden retrasar o que causarán la interrupción de una transferencia posterior.

Para identificar mejor la causa de los picos de transferencia, la API de estadísticas de tráfico permite etiquetar las transferencias de datos que ocurren dentro de un subproceso usando el método TrafficStats.setThreadStatsTag(), seguido de etiquetar y anular etiquetas manualmente de sockets individuales usando TrafficStats.tagSocket() y TrafficStats.untagSocket(). Por ejemplo:

Kotlin

    TrafficStats.setThreadStatsTag(0xF00D)
    TrafficStats.tagSocket(outputSocket)
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket)
    

Java

    TrafficStats.setThreadStatsTag(0xF00D);
    TrafficStats.tagSocket(outputSocket);
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket);
    

La biblioteca HttpURLConnection etiqueta automáticamente los sockets en función del valor actual de TrafficStats.getThreadStatsTag(). La biblioteca también etiqueta y anula las etiquetas de sockets cuando se reciclan a través de grupos keep-alive.

Kotlin

    private class IdentifyTransferSpikeTask : AsyncTask<String, Nothing, String>() {

        override fun onPreExecute() = TrafficStats.setThreadStatsTag(0xF00D)

        override fun doInBackground(vararg urls: String): String {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        override fun onPostExecute(result: String) = TrafficStats.clearThreadStatsTag()
    }
    

Java

    private class IdentifyTransferSpikeTask extends AsyncTask<String, Void, String> {
        @Override
        protected void onPreExecute() {
          TrafficStats.setThreadStatsTag(0xF00D);
        }

        @Override
        protected String doInBackground(String... urls) {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        @Override
        protected void onPostExecute(String result) {
            TrafficStats.clearThreadStatsTag();
       }
    }
    

El etiquetado de sockets es compatible con Android 4.0, pero las estadísticas en tiempo real solo se mostrarán en dispositivos con Android 4.0.3 o versiones posteriores.