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

Mantener la capacidad de respuesta de tu app

Figura 1: Diálogo de ANR que se le muestra al usuario

Es posible escribir código que pase todas las pruebas de rendimiento existentes, pero que, aun así, sea lento, se bloquee o tarde demasiado tiempo en procesar los datos de entrada. El peor escenario que puede presentarse es que tu app responda con un cuadro de diálogo del tipo "Aplicación no responde" (ANR).

En Android, para protegerse contra aplicaciones que no tienen la capacidad de respuesta suficiente por un período de tiempo, el sistema muestra un diálogo que informa que tu app dejó de responder (como el diálogo de la Figura 1). En este punto, tu app dejó de responder por un período de tiempo considerable; por lo tanto, el sistema le ofrece al usuario la posibilidad de cerrarla. Es fundamental incorporar capacidad de respuesta en tu aplicación para que el sistema no muestre un cuadro de diálogo ANR al usuario.

En este documento, se describe la manera en que el sistema Android determina si una aplicación no responde. Además, se presentan pautas para asegurar que tu app no pierda la capacidad de respuesta.

¿Qué activa un ANR?

Generalmente, el sistema muestra un error de ANR si una aplicación no responde a los datos de entrada del usuario. Por ejemplo, si una app bloquea alguna operación de I/O (normalmente, un acceso a la red) en el procesamiento de IU, el sistema no puede procesar los eventos de entrada del usuario. También puede suceder que la app tarde demasiado creando una estructura en memoria elaborada o calculando el siguiente movimiento en un juego en el procesamiento de IU. Es importante asegurarse de que estos cálculos sean eficientes (aunque incluso el código más eficiente requiere tiempo para ejecutarse).

En cualquier situación en la que tu app realice una operación potencialmente larga, no debes realizar el trabajo en el procesamiento de IU, sino crear un subproceso de trabajo y realizar la mayor parte del trabajo en él. De esta forma, el procesamiento de IU (que impulsa el bucle de eventos de la interfaz de usuario) sigue ejecutándose y evita que el sistema llegue a la conclusión de que se bloqueó el código. Como este tipo de procesamiento generalmente se realiza a nivel de la clase, se puede considerar que la capacidad de respuesta es un problema de clase. (Compara este problema con el rendimiento del código básico, que es un problema de método).

En Android, los servicios de sistema Administrador de ventanas y Administrador de actividades supervisan la capacidad de respuesta de una aplicación. Android mostrará el cuadro de diálogo ANR para una aplicación específica cuando detecte una de las siguientes condiciones:

  • No se produce una respuesta a un evento de entrada (como presionar la pantalla o usar las teclas) después de 5 segundos.
  • No terminó de ejecutarse un elemento BroadcastReceiver después de 10 segundos.

¿Cómo evitar los ANR?

Las aplicaciones de Android normalmente se ejecutan completamente en un solo subproceso (de forma predeterminada, en el de "IU" o "principal"). Es decir, cualquier actividad que esté ejecutando la aplicación en el procesamiento de IU que tarda mucho tiempo en completarse puede activar el cuadro de diálogo ANR, ya que la aplicación no puede manejar el evento de entrada ni las transmisiones de intents.

Por lo tanto, cualquier método que se ejecute en el procesamiento de IU debe realizar el menor trabajo posible en ese subproceso. Particularmente, las actividades deben hacer lo mínimo posible para configurarse en métodos de ciclo de vida fundamentales, como onCreate() y onResume(). Las operaciones potencialmente largas (como las de red o base de datos) o los cálculos de mayor exigencia computacional (como el cambio de tamaño de los mapas de bits) deben realizarse en un subproceso de trabajo o, en el caso de las operaciones de bases de datos, a través de una solicitud asíncrona.

La forma más efectiva de crear un subproceso de trabajo para operaciones más largas es con la clase AsyncTask. Simplemente debes extender AsyncTask y, luego, implementar el método doInBackground() para realizar el trabajo. Si quieres publicar los cambios en el progreso, puedes llamar a publishProgress(), que invoca el método de devolución de llamada onProgressUpdate(). Puedes enviar una notificación al usuario desde la implementación de onProgressUpdate() (que se ejecuta en el procesamiento de IU). Por ejemplo:

Kotlin

    private class DownloadFilesTask : AsyncTask<URL, Int, Long>() {

        // Do the long-running work in here
        override fun doInBackground(vararg urls: URL): Long? {
            val count: Float = urls.size.toFloat()
            var totalSize: Long = 0
            urls.forEachIndexed { index, url ->
                totalSize += Downloader.downloadFile(url)
                publishProgress((index / count * 100).toInt())
                // Escape early if cancel() is called
                if (isCancelled) return totalSize
            }
            return totalSize
        }

        // This is called each time you call publishProgress()
        override fun onProgressUpdate(vararg progress: Int?) {
            setProgressPercent(progress.firstOrNull() ?: 0)
        }

        // This is called when doInBackground() is finished
        override fun onPostExecute(result: Long?) {
            showNotification("Downloaded $result bytes")
        }
    }
    

Java

    private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
        // Do the long-running work in here
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
                // Escape early if cancel() is called
                if (isCancelled()) break;
            }
            return totalSize;
        }

        // This is called each time you call publishProgress()
        protected void onProgressUpdate(Integer... progress) {
            setProgressPercent(progress[0]);
        }

        // This is called when doInBackground() is finished
        protected void onPostExecute(Long result) {
            showNotification("Downloaded " + result + " bytes");
        }
    }
    

Para ejecutar este subproceso de trabajo, simplemente debes crear una instancia y llamar a execute():

Kotlin

    DownloadFilesTask().execute(url1, url2, url3)
    

Java

    new DownloadFilesTask().execute(url1, url2, url3);
    

Si bien este enfoque es más complicado que usar AsyncTask, es posible que prefieras crear tu propia clase Thread o HandlerThread. Si lo haces, deberías establecer la prioridad del subproceso en "segundo plano" mediante una llamada a Process.setThreadPriority() y pasar THREAD_PRIORITY_BACKGROUND. Si no estableces que la prioridad sea menor, entonces es posible que el subproceso siga provocando que la app funcione lento, ya que, de forma predeterminada, opera con la misma prioridad que el procesamiento de IU.

Si implementas Thread o HandlerThread, asegúrate de que el procesamiento de IU no se bloquee mientras espera que se complete el subproceso de trabajo y no llame a Thread.wait() ni Thread.sleep(). En lugar de bloquear un proceso mientras espera que finalice el subproceso de trabajo, el subproceso principal debería proporcionar un elemento Handler para que los otros subprocesos envíen de regreso una vez que se complete. Si diseñas tu aplicación de esta manera, el procesamiento de IU de la app conservará su capacidad de respuesta ante las entradas y, por consiguiente, evitará los cuadros de diálogo ANR que se muestran cuando se supera el tiempo de espera de 5 segundos para los eventos de entrada.

La restricción específica del tiempo de ejecución de BroadcastReceiver enfatiza que los receptores de emisión están diseñados para procesar cantidades de trabajo pequeñas y discretas en segundo plano, como guardar una configuración o registrar una Notification. Al igual que con otros métodos llamados en el procesamiento de IU, las aplicaciones deben evitar las operaciones o los cálculos potencialmente largos en un receptor de emisión. Si debe realizarse una acción potencialmente larga como respuesta a la emisión de un intent, en lugar de realizar tareas de alto rendimiento mediante los subprocesos de trabajo, la app debería iniciar un IntentService.

Otro problema frecuente se presenta cuando los objetos BroadcastReceiver se ejecutan con demasiada frecuencia. La ejecución frecuente en segundo plano puede reducir la cantidad de memoria disponible para otras apps. Si quieres obtener más información sobre cómo habilitar o inhabilitar objetos BroadcastReceiver de forma eficaz, consulta Cómo manipular receptores de emisión on demand.

Sugerencia: Puedes usar StrictMode para que te resulte más sencillo encontrar operaciones potencialmente largas (como operaciones de red o base de datos) que es probable que, accidentalmente, estés ejecutando en el subproceso principal.

Cómo reforzar la capacidad de respuesta

Por lo general, los usuarios percibirán lentitud en una aplicación si la capacidad de respuesta es superior a 100 y 200 ms. En consecuencia, a continuación encontrarás algunas sugerencias adicionales más allá de lo que debes hacer para evitar los ANR y para que los usuarios perciban que la capacidad de respuesta de tu aplicación es la adecuada:

  • Si tu app realiza tareas en segundo plano como respuesta a una entrada del usuario, muestra el progreso (por ejemplo, con un elemento ProgressBar en la IU).
  • En el caso de los juegos, realiza los cálculos de los movimientos en un subproceso de trabajo.
  • Si tu aplicación tiene una fase inicial que consume mucho tiempo, considera mostrar una pantalla de presentación o procesar la vista principal lo más pronto posible, indicar que la aplicación está cargando y completar la información de forma asíncrona. En cualquier caso, de alguna manera deberías mostrar el progreso para que el usuario sepa que la aplicación no se bloqueó.
  • Usa herramientas de rendimiento, como Systrace y Traceview, para determinar cuellos de botella en la capacidad de respuesta de tu app.