
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 no respondió durante un período considerable de tiempo, por lo que el sistema le ofrece al usuario la opción de salir de la app. Es fundamental diseñar la capacidad de respuesta de tu aplicación para que el sistema nunca muestre un diálogo de 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 subproceso 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 subproceso 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 de larga duración (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, mediante una solicitud asíncrona.
La forma más eficaz de crear un subproceso de trabajo para operaciones más largas es con la clase AsyncTask
. Simplemente extiende AsyncTask
e implementa el método doInBackground()
para realizar el trabajo.
Si quieres publicar cambios en el progreso para el usuario, 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()
. 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 desees crear tu propia clase Thread
o HandlerThread
. Si lo haces, deberías establecer la prioridad del subproceso en "en segundo plano" mediante un llamado a Process.setThreadPriority()
y pasando THREAD_PRIORITY_BACKGROUND
. Si no estableces que la prioridad sea menor, entonces es posible que el subproceso siga provocando que la app funcione lentamente, ya que, de forma predeterminada, opera con la misma prioridad que el subproceso de IU.
Si implementas Thread
o HandlerThread
, asegúrate de que tu subproceso de IU no se bloquee mientras se completa el subproceso de trabajo y no llames a Thread.wait()
ni a 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 subproceso 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 un elemento Notification
. Al igual que con otros métodos llamados en el subproceso de IU, las aplicaciones deben evitar las operaciones o los cálculos de larga duración en un receptor de emisión. Si debe realizarse una acción de larga duración 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 común 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 transmisión on demand.
Sugerencia: Puedes usar StrictMode
para que te resulte más sencillo encontrar operaciones de larga duración (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.