ANR (vues)

Concepts et implémentation de Jetpack Compose

Lorsque le thread UI d'une application Android est bloqué trop longtemps, une erreur ANR (« L'application ne répond pas ») se déclenche. Si l'application est exécutée au premier plan, le système affiche une boîte de dialogue, comme illustré dans la figure 1. La boîte de dialogue ANR permet à l'utilisateur de forcer l'arrêt de l'application.

Boîte de dialogue ANR affichée à l'intention de l'utilisateur.
Figure 1. Boîte de dialogue ANR affichée à l'utilisateur

Les ANR posent problème, car le thread principal de l'application, responsable de la mise à jour de l'UI, ne peut pas traiter les événements d'entrée utilisateur ni dessiner, ce qui génère de la frustration pour l'utilisateur. Pour en savoir plus sur le thread principal de l'application, consultez Présentation des processus et des threads.

Une erreur ANR se déclenche pour votre application lorsque l'une des conditions suivantes se produit :

  • Délai d'envoi des entrées dépassé : si votre application n'a pas répondu à une entrée (comme un appui sur une touche ou un événement tactile sur l'écran) dans les cinq secondes.
  • Exécution du service : si un service déclaré par votre application ne peut pas terminer l'exécution Service.onCreate() et Service.onStartCommand()/Service.onBind() en quelques secondes.
  • **Service.startForeground() non appelé**: si votre application utilise Context.startForegroundService() pour démarrer un nouveau service au premier plan mais que le service n'appelle pas startForeground() dans les cinq secondes.
  • Diffusion de l'intent : si l'exécution d'un BroadcastReceiver n'est pas terminée dans un délai donné. Si l'application présente une activité au premier plan, ce délai est de cinq secondes.
  • **JobScheduler interactions**: si un JobService ne renvoie pas de JobService.onStartJob() ou JobService.onStopJob() en quelques secondes, ou si une tâche lancée par l'utilisateur démarre et que votre application n'appelle pas JobService.setNotification() quelques secondes après l'appel de JobService.onStartJob(). Pour les applications ciblant Android 13 et les versions antérieures, les erreurs ANR sont silencieuses et ne sont pas signalées à l'application. Pour les applications ciblant Android 14 et versions ultérieures, les erreurs ANR sont explicites et sont signalées à l'application.

Si votre application rencontre des erreurs ANR, vous pouvez suivre les instructions de cet article pour les diagnostiquer et les résoudre.

Résoudre les problèmes

Une fois que vous avez identifié le problème, vous pouvez utiliser les conseils de cette section pour le résoudre.

Lenteur du code dans le thread principal

Identifiez les emplacements de votre code où le thread principal de l'application est occupé pendant plus de cinq secondes. Recherchez les cas d'utilisation suspects dans votre application et essayez de reproduire l'erreur ANR.

Par exemple, la figure 2 illustre une chronologie Traceview dans laquelle le thread principal est occupé pendant plus de cinq secondes.

Figure 2. Chronologie de Traceview montrant un thread principal occupé

Figure 2 : Chronologie de Traceview montrant un thread principal occupé

La figure 2 montre que la majeure partie du code problématique se produit dans le onClick(View) gestionnaire, comme illustré dans l'exemple de code suivant :

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);
}

Dans ce cas, vous devez déplacer la tâche qui s'exécute dans le thread principal vers un thread de nœud de calcul. Le framework Android inclut des classes permettant de déplacer la tâche vers un thread de travail. Pour en savoir plus, consultez Threads de calcul.

E/S sur le thread principal

L'exécution d'opérations d'E/S sur le thread principal est une cause fréquente de lenteur, ce qui peut entraîner des erreurs ANR. Dans Compose, les développeurs déclenchent souvent accidentellement des lectures de disque (comme SharedPreferences ou des appels de base de données) lorsqu'ils tentent de dériver l'état initial.

Exécutez les opérations d'E/S de longue durée en dehors de la couche d'interface utilisateur. Utilisez withContext(Dispatchers.IO) dans un ViewModel ou, mieux encore, utilisez un dépôt sur la couche de données. Il est recommandé de déplacer toutes les opérations d'E/S vers un thread de nœud de calcul, comme indiqué dans la section précédente.

Les opérations réseau et de stockage sont des exemples d'E/S. Pour en savoir plus, consultez Effectuer des opérations réseau et Enregistrer des données.

Conflit de verrouillage

Dans certains cas, la tâche à l'origine de l'erreur ANR n'est pas directement exécutée sur le thread principal de l'application. Si un thread de travail applique un verrouillage sur une ressource dont le thread principal a besoin pour effectuer sa tâche, une erreur ANR peut se produire.

Par exemple, la figure 3 illustre une chronologie Traceview dans laquelle la plupart des tâches sont effectuées sur un thread de travail.

Figure 3 : Chronologie Traceview affichant la tâche en cours d'exécution sur un thread de nœud de calcul

Figure 3 : Chronologie Traceview affichant la tâche en cours d'exécution sur un thread de nœud de calcul

Toutefois, si vos utilisateurs rencontrent toujours des erreurs ANR, vous devez consulter l'état du thread principal dans Android Device Monitor. En règle générale, le thread principal est à l'état RUNNABLE s'il est prêt à mettre à jour l'UI et s'il est généralement responsif.

Toutefois, si le thread principal ne peut pas reprendre l'exécution, cela signifie qu'il est à l'état BLOCKED et qu'il ne peut pas répondre aux événements. L'état indique Monitor (Surveiller) ou Wait (Patienter) dans Android Device Monitor, comme illustré dans la figure 5.

Figure 4 : Thread principal à l'état de surveillance

Figure 4 : Thread principal à l'état de surveillance

La trace suivante montre le thread principal d'une application qui est bloqué en attente d'une ressource :

...
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)
...

L'examen de la trace peut vous aider à localiser le code qui bloque le thread principal. Le code suivant est responsable du verrouillage qui bloque le thread principal dans la trace précédente :

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]);
      }
  }
}

Le thread principal d'une application qui attend le résultat d'un thread de travail constitue un autre exemple, comme illustré dans le code suivant. Notez que l'utilisation de wait() et notify() n'est pas un modèle recommandé dans Kotlin, qui possède ses propres mécanismes de gestion de la simultanéité. Si vous utilisez Kotlin, vous devez si possible utiliser des mécanismes spécifiques à Kotlin.

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();
      }
  }
}

D'autres situations peuvent bloquer le thread principal, y compris les threads qui utilisent Lock et Semaphore, ainsi qu'un pool de ressources (tel qu'un pool de connexions de base de données) ou d'autres mécanismes d'exclusion mutuelle (mutex) .

Vous devez évaluer les verrous que votre application détient sur les ressources en général, mais si vous souhaitez éviter les erreurs ANR, vous devez examiner le verrouillage des ressources requises par le thread principal.

Assurez-vous que les verrous sont conservés pendant le moins de temps possible ou, mieux encore, évaluez si l'application requiert véritablement un blocage. Si vous utilisez le verrouillage pour déterminer quand mettre à jour l'UI en fonction du traitement d'un thread de travail, utilisez des mécanismes tels que onProgressUpdate() et onPostExecute() pour communiquer entre le thread de travail et le thread principal.

Broadcast receivers lents

Les applications peuvent répondre aux messages de diffusion, par exemple en activant ou en désactivant le mode Avion, ou en cas de changement d'état de la connectivité, à l'aide de broadcast receivers. Une erreur ANR se produit lorsqu'une application prend trop de temps pour traiter le message de diffusion.

Une erreur ANR se produit dans les cas suivants :

  • Un broadcast receiver n'a pas fini d'exécuter sa onReceive méthode dans un délai considérable.
  • Un broadcast receiver appelle goAsync et ne parvient pas à appeler finish sur l'objet PendingResult.

Votre application ne doit effectuer que des opérations courtes dans la onReceive méthode de a BroadcastReceiver. Toutefois, si votre application nécessite un traitement plus complexe à la suite d'un message de diffusion, vous devez reporter la tâche vers un ViewModel (en exploitant la puissance des coroutines, des étendues et des dispatchers Kotlin) si la tâche devrait prendre quelques secondes au maximum, tout type de détenteur d'état, ou vers WorkManager pour les tâches qui devraient prendre plus de quelques secondes.

Vous pouvez utiliser des outils comme Traceview pour déterminer si votre broadcast receiver exécute des opérations de longue durée sur le thread principal de l'application. Par exemple, la figure 6 illustre la chronologie d'un broadcast receiver qui traite un message sur le thread principal pendant environ 100 secondes.

Figure 5. Chronologie Traceview affichant la tâche &quot;BroadcastReceiver&quot; sur le thread principal

Figure 5 : Chronologie Traceview affichant la tâche BroadcastReceiver sur le thread principal

Ce comportement peut être dû à l'exécution d'opérations de longue durée sur la onReceive() méthode du BroadcastReceiver, comme illustré dans l' exemple suivant :

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);
}

Dans de telles situations, il est recommandé de déplacer l'opération de longue durée vers un IntentService, puisqu'un thread de travail est utilisé pour exécuter sa tâche. Le code suivant montre comment utiliser un IntentService pour traiter une opération de longue durée :

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);
  }
}

L'utilisation de IntentService permet d'exécuter l'opération de longue durée sur un thread de travail plutôt que sur le thread principal. La figure 7 présente la tâche différée dans le thread de travail dans la chronologie Traceview.

Figure 6. Chronologie Traceview affichant le message de diffusion traité sur un thread de travail

Figure 6 : Chronologie Traceview affichant le message de diffusion traité sur un thread de travail

Votre broadcast receiver peut utiliser goAsync() pour signaler au système qu'il a besoin de plus de temps pour traiter le message. Toutefois, vous devez appeler finish() sur l'PendingResult objet. L'exemple suivant montre comment appeler finish() pour permettre au système de recycler le broadcast receiver et d'éviter une erreur 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);

Toutefois, le déplacement du code d'un broadcast receiver lent vers un autre thread et l'utilisation de goAsync() ne permettent pas de résoudre les erreurs ANR si la diffusion s'exécute en arrière-plan. Le délai avant expiration de l'ANR s'applique toujours.