ANR

Cuando el subproceso de IU de una app de Android se bloquea durante demasiado tiempo, se activa un error del tipo "Aplicación no responde" (ANR). Si la app está en primer plano, el usuario podrá ver un diálogo del sistema, como se observa en la Figura 1. Este diálogo de error de ANR le permite forzar el cierre de la app.

Figura 1: Diálogo de error de ANR que recibe el usuario

Figura 1: Diálogo de error de ANR que recibe el usuario

Los errores de ANR representan un problema porque el subproceso principal de la app, que se encarga de actualizar la IU, no puede procesar eventos de entrada ni obtener datos, lo que le genera frustración al usuario. Para saber más información sobre el subproceso principal de la app, consulta Procesos y subprocesos.

Se activará un error de ANR en tu app cuando ocurra una de las siguientes condiciones:

  • No se produce una respuesta a un evento de entrada (como tocar la pantalla o presionar las teclas) o a BroadcastReceiver después de 5 segundos mientras tu actividad está en primer plano.
  • No terminó de ejecutarse tu componente BroadcastReceiver después de un tiempo considerable mientras tu actividad no está en primer plano.

Si tu app presenta errores de ANR, puedes seguir las indicaciones que se incluyen en este artículo para diagnosticar el problema y corregirlo.

Cómo detectar y diagnosticar problemas

Android ofrece varios medios para avisarte cuando tu app tiene un problema y para ayudarte a diagnosticarlo. Si ya la publicaste, Android vitals puede alertarte cuando se produce un problema y cuenta con herramientas de diagnóstico para ayudarte a encontrarlo.

Android vitals

Android vitals puede ayudarte a mejorar el rendimiento de tu app. Para ello, te envía alertas a través de Play Console cuando tu app presenta una cantidad excesiva de errores de ANR. Android vitals considera que una app presenta errores de ANR de manera excesiva en los siguientes casos:

  • Cuando presenta un error de ANR una vez, como mínimo, en al menos el 0.47% de sus sesiones diarias.
  • Cuando presenta dos o más errores de ANR en al menos el 0.24% de sus sesiones diarias.

Una sesión diaria corresponde a un día en el que se usó tu app.

Si deseas obtener información sobre cómo Google Play recopila datos de Android vitals, consulta la documentación de Play Console.

Cómo diagnosticar los errores de ANR

Al diagnosticar los errores de ANR, debes tener en cuenta algunos patrones comunes:

  1. La app realiza operaciones lentas de E/S en el subproceso principal.
  2. La app realiza un cálculo largo en el subproceso principal.
  3. El subproceso principal realiza una llamada síncrona de Binder a otro proceso, y dicho proceso tarda mucho tiempo en responder.
  4. El subproceso principal está bloqueado a la espera de un bloque sincronizado para una operación larga que se produce en otro subproceso.
  5. El subproceso principal se interbloqueó con otro subproceso, ya sea en tu proceso o mediante una llamada a Binder. El subproceso principal no solo está a la espera de que termine una operación larga, sino que se interbloqueó. Para obtener más información, consulta Interbloqueo en Wikipedia.

Las siguientes técnicas pueden ayudarte a descubrir cuál de estas causas provoca los errores de ANR.

Modo estricto

Al usar StrictMode, puedes encontrar las operaciones de E/S con accidentes en el subproceso principal mientras desarrollas tu app. Puedes usar StrictMode a nivel de la aplicación o la actividad.

Cómo habilitar diálogos de errores de ANR en segundo plano

Android muestra los diálogos de errores de ANR cuando las app tardan demasiado en procesar el mensaje de emisión solo si está habilitada la opción Mostrar ARN en Opciones para desarrolladores del dispositivo. Por esta razón, el usuario no siempre recibirá los diálogos de errores de ANR en segundo plano, si bien es posible que la app aún presente problemas de rendimiento.

Traceview

Puedes usar Traceview para mantener un registro de tu app en ejecución mientras analizas los casos prácticos y, además, identificas los lugares donde está ocupado el subproceso principal. Para obtener información sobre cómo usar Traceview, consulta Cómo generar perfiles con Traceview y dmtracedump.

Cómo extraer un archivo de registro

Android almacena la información de registro cuando presenta un error de ANR. En las versiones anteriores de SO, hay un único archivo /data/anr/traces.txt en el dispositivo. En las versiones más recientes, hay varios archivos /data/anr/anr_*. Puedes acceder a los registros de errores de ANR desde un dispositivo o emulador. Para ello, utiliza Android Debug Bridge (adb) como raíz:

adb root
    adb shell ls /data/anr
    adb pull /data/anr/<filename>
    

Puedes capturar un informe de errores desde un dispositivo físico mediante la opción para desarrolladores Iniciar informe de errores en el dispositivo o con el comando adb bugreport en tu máquina de desarrollo. Para obtener más información, consulta Cómo capturar y leer informes de errores.

Cómo corregir problemas

Una vez que hayas identificado el problema, puedes usar las sugerencias incluidas en esta sección para corregir problemas habituales.

Código lento en el subproceso principal

Identifica las partes de tu código donde el subproceso principal de la app está ocupado durante más de 5 segundos. Busca los casos prácticos sospechosos en tu app y, luego, intenta reproducir el error de ANR.

Por ejemplo, en la Figura 2, se muestra un cronograma de Traceview donde el subproceso principal está ocupado durante más de 5 segundos.

Figura 2: Cronograma de Traceview que muestra un subproceso principal ocupado

Figura 2: Cronograma de Traceview que muestra un subproceso principal ocupado

En la Figura 2, se muestra que la mayor parte del código incorrecto ocurre en el controlador onClick(View), como se observa en el siguiente ejemplo de código:

Kotlin

    override fun onClick(v: View) {
        // This task runs on the main thread.
        BubbleSort.sort(data)
    }
    

Java

    @Override
    public void onClick(View view) {
        // This task runs on the main thread.
        BubbleSort.sort(data);
    }
    

En este caso, debes pasar el trabajo que se ejecuta en el subproceso principal a un subproceso de trabajo. El marco de trabajo de Android incluye clases que pueden ayudarte a pasar la tarea a un subproceso de trabajo. Para obtener más información, consulta las Clases de ayuda para subprocesos. En el siguiente código, se muestra cómo usar la clase de ayuda AsyncTask para procesar la tarea en un subproceso de trabajo:

Kotlin

    override fun onClick(v: View) {
        // The long-running operation is run on a worker thread
        object : AsyncTask<Array<Int>, Int, Long>() {
            override fun doInBackground(vararg params: Array<Int>): Long? =
                BubbleSort.sort(params[0])

        }.execute(data)
    }
    

Java

    @Override
    public void onClick(View view) {
       // The long-running operation is run on a worker thread
       new AsyncTask<Integer[], Integer, Long>() {
           @Override
           protected Long doInBackground(Integer[]... params) {
               BubbleSort.sort(params[0]);
           }
       }.execute(data);
    }
    

Traceview muestra que la mayor parte del código se ejecuta en un subproceso de trabajo, como se observa en la Figura 3. El subproceso principal está disponible para dar respuesta a los eventos del usuario.

Figura 3: Cronograma de Traceview que muestra el trabajo que controla un subproceso de trabajo

Figura 3: Cronograma de Traceview que muestra el trabajo que controla un subproceso de trabajo

E/S en el subproceso principal

Cuando se ejecutan operaciones de E/S en el subproceso principal, las operaciones se ralentizan y se pueden producir errores de ANR. Se recomienda pasar todas las operaciones de E/S a un subproceso de trabajo, como se muestra en la sección anterior.

Las operaciones de red y almacenamiento son algunos ejemplos de operaciones de E/S. Para obtener más información, consulta Cómo llevar a cabo operaciones de red y Cómo guardar datos.

Contención de bloqueo

En algunas situaciones, el trabajo que ocasiona el error de ANR no se ejecuta de forma directa en el subproceso principal de la app. Si un subproceso de trabajo bloquea un recurso que requiere el subproceso principal para completar su trabajo, puede producirse un error de ANR.

Por ejemplo, en la Figura 4, se muestra un cronograma de Traceview donde la mayor parte del trabajo se realiza en un subproceso de trabajo.

Figura 4: Cronograma de Traceview que muestra el trabajo que se ejecuta en un subproceso de trabajo

Figura 4: Cronograma de Traceview que muestra el trabajo que se ejecuta en un subproceso de trabajo

Sin embargo, si tus usuarios siguen teniendo errores de ANR, debes analizar el estado del subproceso principal en Android Device Monitor. Por lo general, el subproceso principal tiene estado RUNNABLE si está listo para actualizar la IU y tiene capacidad de respuesta.

Sin embargo, si el subproceso principal no puede reanudar la ejecución, tiene el estado BLOCKED y no puede responder a los eventos. En Android Device Monitor, el estado aparece como Monitor o Wait, como se muestra en la Figura 5.

Figura 5: Subproceso principal en el estado de Monitor

Figura 5: Subproceso principal en el estado Monitor

En el siguiente registro, se muestra el subproceso principal de una app que está bloqueado a la espera de un recurso:

...
    AsyncTask #2" prio=5 tid=18 Runnable
      | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
      | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
      | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
      | stack=0x94a7e000-0x94a80000 stackSize=1038KB
      | held mutexes= "mutator lock"(shared held)
      at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
      at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
      - locked <0x083105ee> (a java.lang.Boolean)
      at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
      at android.os.AsyncTask$2.call(AsyncTask.java:305)
      at java.util.concurrent.FutureTask.run(FutureTask.java:237)
      at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
      at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
      at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
      at java.lang.Thread.run(Thread.java:761)
    ...
    

Al revisar el registro, podrás encontrar el código que bloquea el subproceso principal. El siguiente código contiene el bloqueo que impide el subproceso principal en el registro anterior:

Kotlin

    override fun onClick(v: View) {
        // The worker thread holds a lock on lockedResource
        LockTask().execute(data)

        synchronized(lockedResource) {
            // The main thread requires lockedResource here
            // but it has to wait until LockTask finishes using it.
        }
    }

    class LockTask : AsyncTask<Array<Int>, Int, Long>() {
        override fun doInBackground(vararg params: Array<Int>): Long? =
                synchronized(lockedResource) {
                    // This is a long-running operation, which makes
                    // the lock last for a long time
                    BubbleSort.sort(params[0])
                }
    }
    

Java

    @Override
    public void onClick(View v) {
        // The worker thread holds a lock on lockedResource
       new LockTask().execute(data);

       synchronized (lockedResource) {
           // The main thread requires lockedResource here
           // but it has to wait until LockTask finishes using it.
       }
    }

    public class LockTask extends AsyncTask<Integer[], Integer, Long> {
       @Override
       protected Long doInBackground(Integer[]... params) {
           synchronized (lockedResource) {
               // This is a long-running operation, which makes
               // the lock last for a long time
               BubbleSort.sort(params[0]);
           }
       }
    }
    

Otro ejemplo es el subproceso principal de una app a la espera de un resultado de un subproceso de trabajo, como se muestra en el siguiente código. Ten en cuenta que no se recomienda usar los patrones wait() y notify() en Kotlin, ya que tiene sus propios mecanismos para procesar la simultaneidad. Al usar Kotlin, debes emplear mecanismos específicos de Kotlin cuando sea posible.

Kotlin

    fun onClick(v: View) {
        val lock = java.lang.Object()
        val waitTask = WaitTask(lock)
        synchronized(lock) {
            try {
                waitTask.execute(data)
                // Wait for this worker thread’s notification
                lock.wait()
            } catch (e: InterruptedException) {
            }
        }
    }

    internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
        override fun doInBackground(vararg params: Array<Int>): Long? {
            synchronized(lock) {
                BubbleSort.sort(params[0])
                // Finished, notify the main thread
                lock.notify()
            }
        }
    }
    

Java

    public void onClick(View v) {
       WaitTask waitTask = new WaitTask();
       synchronized (waitTask) {
           try {
               waitTask.execute(data);
               // Wait for this worker thread’s notification
               waitTask.wait();
           } catch (InterruptedException e) {}
       }
    }

    class WaitTask extends AsyncTask<Integer[], Integer, Long> {
       @Override
       protected Long doInBackground(Integer[]... params) {
           synchronized (this) {
               BubbleSort.sort(params[0]);
               // Finished, notify the main thread
               notify();
           }
       }
    }
    

Existen otras situaciones que pueden bloquear el subproceso principal, incluidos los subprocesos que usan Lock, Semaphore, así como un grupo de recursos (como un grupo de conexiones de bases de datos) o, bien, otros mecanismos de exclusión mutua.

Debes evaluar los bloqueos que tiene tu app en los recursos generales. Sin embargo, si deseas evitar los errores de ANR, debes analizar los bloqueos retenidos para los recursos que requiere el subproceso principal.

Asegúrate de que los bloqueos se mantengan durante el menor tiempo posible o, mejor aún, evalúa si la app debe retenerlos en primer lugar. Si usas el bloqueo para determinar cuándo actualizar la IU según el procesamiento de un subproceso de trabajo, emplea los mecanismos onProgressUpdate() y onPostExecute() para establecer la comunicación entre el subproceso principal y el de trabajo.

Interbloqueos

Se produce un interbloqueo cuando un subproceso entra en estado de espera porque otro subproceso retiene un recurso requerido, y este otro subproceso también está a la espera del recurso que retiene el primer subproceso. Si esta situación ocurre el subproceso principal de la app, es probable que se produzca un error de ANR.

Los interbloqueos son un fenómeno muy estudiado en la informática, y existen algoritmos de prevención de interbloqueos que puedes aplicar para evitarlos.

Para obtener más información, consulta Interbloqueo y Algoritmos de prevención de interbloqueos en Wikipedia.

Receptores de emisión lenta

A través de los receptores de emisión, las app pueden responder a los mensajes de emisión, como habilitar o no el modo de avión o un cambio en el estado de conectividad. Los errores de ANR se producen cuando una app tarda demasiado en procesar el mensaje de emisión.

Los errores de ANR se producen en los siguientes casos:

  • Cuando un receptor de emisión no terminó de ejecutar su método onReceive() en un lapso considerable.
  • Cuando un receptor de emisión llama a goAsync(), pero no puede llamar a finish() en el objeto PendingResult.

Tu app solo debe realizar operaciones cortas en el método onReceive() de un BroadcastReceiver. Sin embargo, si requiere un procesamiento más complejo como resultado de un mensaje de emisión, debes diferir la tarea a IntentService.

Puedes usar herramientas como Traceview para identificar si tu receptor de emisión ejecuta operaciones prolongadas en el subproceso principal de la app. Por ejemplo, en la Figura 6, se muestra el cronograma de un receptor de emisión que procesa un mensaje en el subproceso principal durante aproximadamente 100 segundos.

Figura 6: Cronograma de Traceview que muestra el trabajo de BroadcastReceiver en el subproceso principal

Figura 6: Cronograma de Traceview que muestra el trabajo de BroadcastReceiver en el subproceso principal

Este comportamiento puede deberse a la ejecución de operaciones prolongadas en el método onReceive() de BroadcastReceiver, como se observa en el siguiente ejemplo:

Kotlin

    override fun onReceive(context: Context, intent: Intent) {
        // This is a long-running operation
        BubbleSort.sort(data)
    }
    

Java

    @Override
    public void onReceive(Context context, Intent intent) {
        // This is a long-running operation
        BubbleSort.sort(data);
    }
    

En estas situaciones, se recomienda pasar la operación prolongada a IntentService, ya que utiliza un subproceso de trabajo para ejecutar su trabajo. En el siguiente código, se muestra cómo usar IntentService para procesar una operación prolongada:

Kotlin

    override fun onReceive(context: Context, intent: Intent) {
        Intent(context, MyIntentService::class.java).also { intentService ->
            // The task now runs on a worker thread.
            context.startService(intentService)
        }
    }

    class MyIntentService : IntentService("MyIntentService") {
        override fun onHandleIntent(intent: Intent?) {
            BubbleSort.sort(data)
        }
    }
    

Java

    @Override
    public void onReceive(Context context, Intent intent) {
        // The task now runs on a worker thread.
        Intent intentService = new Intent(context, MyIntentService.class);
        context.startService(intentService);
    }

    public class MyIntentService extends IntentService {
       @Override
       protected void onHandleIntent(@Nullable Intent intent) {
           BubbleSort.sort(data);
       }
    }
    

Al usar IntentService, la operación prolongada se ejecuta en un subproceso de trabajo en lugar del subproceso principal. En la Figura 7, se muestra el trabajo diferido al subproceso de trabajo en el cronograma de Traceview.

Figura 7: Cronograma de Traceview que muestra el mensaje de emisión que se procesa en un subproceso de trabajo

Figura 7: Cronograma de Traceview que muestra el mensaje de emisión procesado en un subproceso de trabajo

Tu receptor de emisión puede usar goAsync() para indicarle al sistema que necesita más tiempo para procesar el mensaje. Sin embargo, debes llamar a finish() en el objeto PendingResult. En el siguiente ejemplo, se muestra cómo llamar a finish() para que el sistema recicle el receptor de emisión a fin de evitar un error de ANR:

Kotlin

    val pendingResult = goAsync()

    object : AsyncTask<Array<Int>, Int, Long>() {
        override fun doInBackground(vararg params: Array<Int>): Long? {
            // This is a long-running operation
            BubbleSort.sort(params[0])
            pendingResult.finish()
            return 0L
        }
    }.execute(data)
    

Java

    final PendingResult pendingResult = goAsync();
    new AsyncTask<Integer[], Integer, Long>() {
       @Override
       protected Long doInBackground(Integer[]... params) {
           // This is a long-running operation
           BubbleSort.sort(params[0]);
           pendingResult.finish();
       }
    }.execute(data);
    

Sin embargo, al pasar el código de un receptor de emisión lenta a otro subproceso y usar goAsync(), no se corregirá el erro de ANR si la emisión está en segundo plano. Aún se aplica el tiempo de espera del error de ANR.

Para obtener más información sobre los errores de ANR, consulta Cómo mantener la capacidad de respuesta de tu app. Para obtener más información sobre los subprocesos, consulta Rendimiento de los subprocesos.