Blocages

Une application Android plante lorsqu'une interruption inattendue est provoquée par une exception ou un signal non géré. Une application écrite en Java ou Kotlin plante si elle génère une exception non gérée, représentée par la classe Throwable. Une application écrite à l'aide de code machine ou C++ plante en cas de signal non géré, tel que SIGSEGV, pendant son exécution.

Lorsqu'une application plante, Android met fin au processus et affiche une boîte de dialogue pour informer l'utilisateur que l'application s'est arrêtée, comme illustré dans la figure 1.

Plantage d'une application sur un appareil Android

Figure 1. Plantage d'une application sur un appareil Android

Une application peut planter même si elle ne s'exécute pas au premier plan. Tout composant d'une application, même s'il s'exécute en arrière-plan, comme des broadcast receivers ou les fournisseurs de contenu, peut entraîner le plantage de l'application. Ces plantages sont souvent déroutants, car ils surviennent à un moment où les utilisateurs n'étaient pas activement en train d'agir avec votre application.

Si votre application plante, vous pouvez suivre les instructions de cette page pour diagnostiquer et résoudre le problème.

Détecter le problème

Vous ne pouvez pas toujours savoir que vos utilisateurs subissent des plantages avec votre application. Si vous l'avez déjà publiée, Android Vitals vous permet de consulter ses taux de plantages.

Android Vitals

Android Vitals vous permet de surveiller et d'améliorer le taux de plantages de votre application. Plusieurs taux de plantages sont mesurés :

  • Taux de plantages : pourcentage d'utilisateurs actifs par jour ayant subi un plantage.
  • Taux de plantages repérés par l'utilisateur : pourcentage d'utilisateurs actifs par jour ayant subi au moins un plantage lorsqu'ils se servaient activement de votre application (plantage repéré par l'utilisateur). Une application est considérée comme active si elle affiche une activité ou exécute un service de premier plan.

  • Taux de plantages multiples : pourcentage d'utilisateurs par jour ayant subi au moins deux plantages.

Un utilisateur actif par jour est un utilisateur unique qui se sert de votre application un seul jour sur un seul appareil, avec éventuellement plusieurs sessions. Si un utilisateur se sert de votre application sur plusieurs appareils au cours d'une même journée, chaque appareil est comptabilisé dans le nombre d'utilisateurs actifs pour ce jour-là. Si plusieurs personnes utilisent le même appareil au cours d'une même journée, un seul utilisateur actif est comptabilisé.

Le taux de plantage repéré par l'utilisateur est une statistique principale Android Vitals. Il affecte donc la visibilité de votre application sur Google Play. Cette métrique est importante, car les plantages comptabilisés se produisent toujours lorsque l'utilisateur interagit avec l'application, ce qui entraîne le plus de perturbations.

Play a défini deux seuils de comportement insatisfaisant pour cette métrique :

  • Seuil général de comportement insatisfaisant : au moins 1,09 % des utilisateurs actifs par jour subissent un plantage qu'ils repèrent eux-mêmes sur tous les modèles d'appareils.
  • Seuil de comportement insatisfaisant par appareil : au moins 8 % des utilisateurs actifs par jour subissent un plantage qu'ils repèrent eux-mêmes sur un même modèle d'appareil.

Si votre application dépasse le seuil général de comportement insatisfaisant, elle risque d'être moins visible sur tous les appareils. Si elle dépasse le seuil de comportement insatisfaisant par appareil, elle risque d'être moins visible sur le type d'appareil en question et d'afficher un avertissement dans votre fiche Play Store.

Android Vitals peut vous envoyer des alertes via la Play Console lorsque votre application subit un nombre excessif de plantages.

Pour savoir comment Google Play collecte les données Android Vitals, consultez la documentation de la Play Console.

Diagnostiquer les plantages

Une fois que vous avez identifié que votre application signale des plantages, l'étape suivante consiste à les diagnostiquer. Il peut être difficile de trouver une solution aux plantages. Toutefois, si vous pouvez en identifier la cause première, vous parviendrez sans doute à résoudre ce problème.

De nombreuses situations peuvent entraîner le plantage de votre application. Il peut s'agir de raisons simples, comme la rencontre d'une valeur nulle ou d'une chaîne vide, mais d'autres sont moins évidentes, comme la transmission d'arguments non valides à une API, voire des interactions multithread complexes.

Les plantages sur Android génèrent une trace de la pile, c'est-à-dire un instantané de la séquence des fonctions imbriquées appelées dans votre programme jusqu'au moment du plantage. Vous pouvez consulter les traces de la pile de plantage dans Android Vitals.

Lire une trace de la pile

Pour résoudre un plantage, la première étape consiste à identifier l'endroit où il se produit. Vous pouvez utiliser la trace de la pile disponible dans les détails du rapport si vous utilisez la Play Console ou la sortie de l'outil Logcat. Si aucune trace de la pile n'est disponible, vous devez reproduire le plantage localement, en testant l'application manuellement ou en contactant les utilisateurs concernés, puis en le reproduisant tout en utilisant Logcat.

La trace ci-dessous montre un exemple de plantage d'une application écrite à l'aide du langage de programmation Java :

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException: crash sample
at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
at android.view.View.performClick(View.java:6134)
at android.view.View$PerformClick.run(View.java:23965)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6440)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)
--------- beginning of system

Une trace de la pile affiche deux informations essentielles au débogage d'un plantage :

  • Type d'exception générée
  • Section de code où l'exception est générée

Le type d'exception générée est généralement très révélateur. Vérifiez s'il s'agit d'une exception IOException, OutOfMemoryError ou autre, puis recherchez la documentation sur la classe d'exception concernée.

La classe, la méthode, le fichier et le numéro de ligne du fichier source où l'exception est générée figure sur la deuxième ligne d'une trace de la pile. Pour chaque fonction appelée, une ligne indique le site de l'appel précédent (appelé "bloc de pile"). En parcourant la pile et en examinant le code, vous trouverez peut-être un emplacement qui transmet une valeur incorrecte. Si votre code n'apparaît pas dans la trace de la pile, vous avez probablement transmis un paramètre non valide à une opération asynchrone. Vous pouvez généralement comprendre ce qui s'est passé en examinant chaque ligne de la trace de la pile, en recherchant les classes d'API que vous avez utilisées et en confirmant que les paramètres transmis étaient corrects et que vos appels provenaient d'un emplacement autorisé.

Les traces de pile pour les applications avec du code C et C++ fonctionnent de la même manière.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
Timestamp: 2020-02-16 11:16:31+0100
pid: 8288, tid: 8288, name: com.example.testapp  >>> com.example.testapp <<<
uid: 1010332
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
    x0  0000007da81396c0  x1  0000007fc91522d4  x2  0000000000000001  x3  000000000000206e
    x4  0000007da8087000  x5  0000007fc9152310  x6  0000007d209c6c68  x7  0000007da8087000
    x8  0000000000000000  x9  0000007cba01b660  x10 0000000000430000  x11 0000007d80000000
    x12 0000000000000060  x13 0000000023fafc10  x14 0000000000000006  x15 ffffffffffffffff
    x16 0000007cba01b618  x17 0000007da44c88c0  x18 0000007da943c000  x19 0000007da8087000
    x20 0000000000000000  x21 0000007da8087000  x22 0000007fc9152540  x23 0000007d17982d6b
    x24 0000000000000004  x25 0000007da823c020  x26 0000007da80870b0  x27 0000000000000001
    x28 0000007fc91522d0  x29 0000007fc91522a0
    sp  0000007fc9152290  lr  0000007d22d4e354  pc  0000007cba01b640

backtrace:
  #00  pc 0000000000042f89  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::Crasher::crash() const)
  #01  pc 0000000000000640  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::runCrashThread())
  #02  pc 0000000000065a3b  /system/lib/libc.so (__pthread_start(void*))
  #03  pc 000000000001e4fd  /system/lib/libc.so (__start_thread)

Si vous ne voyez pas d'informations au niveau de la classe et de la fonction dans les traces de la pile native, vous devrez peut-être générer un fichier de décodage natif, puis l'importer dans la Google Play Console. Pour en savoir plus, consultez Désobscurcir les traces de la pile de plantage. Pour obtenir des informations générales sur les plantages natifs, consultez la page concernant le diagnostic des plantages natifs.

Conseils pour reproduire un plantage

Il est possible que vous ne puissiez pas reproduire le problème simplement en démarrant un émulateur ou en connectant votre appareil à votre ordinateur. Les environnements de développement ont tendance à disposer de plus de ressources (bande passante, mémoire et espace de stockage, par exemple). Utilisez le type d'exception pour déterminer si le problème est dû à une ressource insuffisante (et identifier laquelle) ou pour trouver une corrélation entre la version d'Android, le type d'appareil et la version de votre application.

Erreurs de mémoire

Si vous obtenez une alerte OutOfMemoryError, vous pouvez créer un émulateur avec une faible capacité de mémoire afin de réaliser vos tests. La figure 2 montre les paramètres AVD Manager, où vous pouvez contrôler la quantité de mémoire sur l'appareil.

Paramètre de mémoire sur AVD Manager

Figure 2 : Paramètre de mémoire sur AVD Manager

Exceptions réseau

Étant donné que les utilisateurs passent fréquemment d'un réseau mobile ou Wi-Fi à un autre, les exceptions réseau d'une application ne doivent généralement pas être considérées comme des erreurs, mais comme des conditions de fonctionnement normales se produisant de manière inattendue.

Si vous devez reproduire une exception réseau, comme UnknownHostException, essayez d'activer le mode Avion pendant que votre application tente d'utiliser le réseau.

Une autre option consiste à réduire la qualité du réseau dans l'émulateur en choisissant une émulation de débit réseau et/ou un délai de réseau. Vous pouvez utiliser les paramètres Speed (Vitesse) et Latency (Latence) d'AVD Manager, ou démarrer l'émulateur avec les options -netdelay et -netspeed, comme dans l'exemple de ligne de commande suivant :

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

Cet exemple définit un délai de 20 secondes pour toutes les requêtes réseau et une vitesse d'importation et de téléchargement de 14,4 Kbit/s. Pour en savoir plus sur les options de ligne de commande de l'émulateur, consultez la section consacrée au démarrage de l'émulateur à partir de la ligne de commande.

Lire avec Logcat

Une fois que vous savez comment reproduire le plantage, vous pouvez utiliser un outil tel que logcat pour obtenir plus d'informations.

La sortie Logcat affiche les autres messages de journal que vous avez imprimés, ainsi que des messages du système. N'oubliez pas de désactiver les instructions Log supplémentaires que vous pouvez avoir ajoutées, car l'impression sollicite le processeur et la batterie pendant l'exécution de votre application.

Éviter les plantages causés par les exceptions de pointeur nul

Les exceptions de pointeur nul (identifiées par le type d'erreur d'exécution NullPointerException) se produisent lorsque vous essayez d'accéder à un objet nul, généralement en appelant ses méthodes ou en accédant à ses membres. Les exceptions de pointeur nul sont la cause principale de plantages des applications sur Google Play. La valeur "null" sert à indiquer que l'objet est manquant (par exemple, il n'a pas encore été créé ou attribué). Pour éviter les exceptions de pointeur nul, vous devez vous assurer que les références d'objet avec lesquelles vous travaillez ne sont pas nulles avant d'appeler des méthodes les exploitant ou d'essayer d'accéder à leurs membres. Si la référence d'objet est nulle, traitez ce cas correctement (par exemple, quittez une méthode avant d'effectuer une opération sur la référence d'objet et écrivez les informations utiles dans un journal de débogage).

Comme il ne serait pas raisonnable de vérifier qu'aucune valeur n'est nulle pour chaque paramètre de chaque méthode appelée, vous pouvez vous fier à l'IDE ou au type d'objet pour indiquer la possibilité de valeur nulle.

Langage de programmation Java

Les sections suivantes concernent le langage de programmation Java.

Avertissements au moment de la compilation

Annotez les paramètres de vos méthodes et renvoyez des valeurs avec @Nullable et @NonNull pour recevoir des avertissements de l'IDE au moment de la compilation. Les avertissements suivants vous signalent qu'un objet présente un risque de valeur nulle :

Avertissement d&#39;exception de pointeur nul

Ces vérifications concernent des objets dont la valeur peut être nulle. Une exception sur un objet @NonNull vous indique que votre code comporte une erreur à corriger.

Erreurs au moment de la compilation

Étant donné que la possibilité d'une valeur nulle doit jouer un rôle utile, vous pouvez l'intégrer aux types que vous utilisez pour qu'une vérification ait lieu au moment de la compilation. Si vous savez qu'un objet risque d'être nul et que la possibilité de valeur nulle doit être gérée, vous pouvez l'encapsuler dans un objet tel que Optional. Il faut toujours privilégier les types indiquant la possibilité de valeur nulle.

Kotlin

En Kotlin, la possibilité de valeur nulle fait partie du système de types. Par exemple, une variable doit être déclarée dès le départ comme pouvant ou ne pouvant pas être nulle Les types pouvant être de valeur nulle sont marqués d'un ? :

// non-null
var s: String = "Hello"

// null
var s: String? = "Hello"

Les variables sans possibilité de valeur nulle ne peuvent pas recevoir une telle valeur. Les autres doivent faire l'objet d'une vérification avant d'être utilisées comme non nulles.

Si vous ne souhaitez pas rechercher les valeurs nulles explicitement, vous pouvez utiliser l'opérateur d'appel sécurisé ?. :

val length: Int? = string?.length  // length is a nullable int
                                   // if string is null, then length is null

Une bonne pratique consiste à anticiper le cas d'une valeur nulle pour un objet pouvant avoir une telle valeur, faute de quoi votre application risque de présenter des états inattendus. Si votre application ne plante plus du fait d'erreurs NullPointerException, vous ne saurez pas que ces erreurs existent.

Il existe plusieurs façons de vérifier les valeurs nulles :

  • Contrôles if

    val length = if(string != null) string.length else 0
    

    En raison de la fonctionnalité Smart-Cast et de la vérification des valeurs nulles, le compilateur Kotlin sait que la valeur de la chaîne est non nulle. Vous pouvez donc utiliser la référence directement, sans recourir à l'opérateur d'appel sécurisé.

  • Opérateur Elvis ?:

    Cet opérateur vous permet de déclarer "si l'objet n'est pas nul, renvoyer l'objet ; sinon, renvoyer quelque chose d'autre".

    val length = string?.length ?: 0
    

Il reste possible d'obtenir un NullPointerException dans Kotlin. Voici les cas les plus fréquents où cela se produit :

  • Lorsque vous générez explicitement un NullPointerException.
  • Lorsque vous utilisez l'opérateur !! d'assertion nulle. Cet opérateur convertit n'importe quelle valeur vers un type non nul, en générant NullPointerException si la valeur est nulle.
  • Lorsque vous accédez à une référence nulle d'un type de plate-forme.

Types de plates-formes

Les types de plates-formes sont des déclarations d'objets provenant de Java. Ces types font l'objet d'un traitement spécial ; les vérifications des valeurs nulles ne sont pas appliquées aussi systématiquement. La garantie non nulle est donc identique à celle de Java. Lorsque vous accédez à une référence de type de plate-forme, Kotlin ne crée pas d'erreurs au moment de la compilation, mais ces références peuvent donner lieu à des erreurs d'exécution. Consultez l'exemple suivant dans la documentation Kotlin :

val list = ArrayList<String>() // non-null (constructor result) list.add("Item")
val size = list.size // non-null (primitive int) val item = list[0] // platform
type inferred (ordinary Java object) item.substring(1) // allowed, may throw an
                                                       // exception if item == null

Kotlin s'appuie sur l'inférence de type lorsqu'une valeur de plate-forme est attribuée à une variable Kotlin. Vous pouvez également définir le type attendu. Le meilleur moyen de garantir qu'une référence en provenance de Java se trouve dans l'état souhaité concernant la possibilité de valeur nulle consiste à utiliser des annotations le précisant (par exemple, @Nullable) dans votre code Java. Le compilateur Kotlin représentera ces références comme des types pouvant être nuls ou non plutôt que comme des types de plates-formes.

Les API Java Jetpack ont été annotées avec @Nullable ou @NonNull selon les besoins, et une approche similaire a été adoptée dans le SDK Android 11. Les types provenant de ce SDK et utilisés en Kotlin seront représentés par des types valides pouvant ou ne pouvant pas être nuls.

Grâce à ce système de types de Kotlin, nous avons constaté que les applications présentaient une réduction importante du nombre de plantages dus à l'erreur NullPointerException. Par exemple, on a constaté sur l'application Google Home une réduction de 30 % des plantages causés par des exceptions de pointeur nul pendant l'année où a été effectuée la migration du développement de nouvelles fonctionnalités vers Kotlin.