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.
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.
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 :
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é.
-
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érantNullPointerException
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.