Rileva e diagnostica gli arresti anomali

Un'app per Android si arresta in modo anomalo quando si verifica un'uscita imprevista causata da un'eccezione o da un indicatore non gestito. Un'app scritta utilizzando Java o Kotlin si arresta in modo anomalo se genera un'eccezione non gestita, rappresentata dalla classe Throwable. Un'app scritta utilizzando codice macchina o C++ si arresta in modo anomalo se durante l'esecuzione viene rilevato un indicatore non gestito, ad esempio SIGSEGV.

Quando un'app si arresta in modo anomalo, Android ne interrompe il processo e mostra una finestra di dialogo per comunicare all'utente che l'app è stata interrotta, come mostrato nella figura 1.

Un arresto anomalo di un'app su un dispositivo Android

Figura 1. Un arresto anomalo di un'app su un dispositivo Android

Non è necessario che un'app sia in esecuzione in primo piano per arrestarsi in modo anomalo. Qualsiasi componente dell'app, anche quelli come ricevitori di trasmissioni o fornitori di contenuti in esecuzione in background, può causare l'arresto anomalo di un'app. Questi arresti anomali sono spesso poco chiari per gli utenti, perché non hanno interagito attivamente con la tua app.

Se la tua app presenta arresti anomali, puoi utilizzare le indicazioni in questa pagina per diagnosticare e risolvere il problema.

Rileva il problema

Potresti non sapere sempre che i tuoi utenti riscontrano arresti anomali quando utilizzano la tua app. Se hai già pubblicato la tua app, puoi usare Android vitals per visualizzare le relative percentuali.

Android vitals

Android vitals può aiutarti a monitorare e migliorare la percentuale di arresti anomali della tua app. Android vitals misura diverse percentuali di arresti anomali:

  • Percentuale di arresti anomali: la percentuale di utenti attivi giornalieri che hanno riscontrato qualsiasi tipo di arresto anomalo.
  • Percentuale di arresti anomali percepiti dall'utente: la percentuale di utenti attivi giornalieri che hanno riscontrato almeno un arresto anomalo mentre utilizzavano attivamente la tua app (arresto anomalo percepito dall'utente). Un'app è considerata in uso attivo se mostra attività o esegue servizi in primo piano.

  • Percentuale di arresti anomali multipli: la percentuale di utenti attivi giornalieri che hanno riscontrato almeno due arresti anomali.

Un utente attivo giornaliero è un utente unico che utilizza la tua app in un singolo giorno su un singolo dispositivo, potenzialmente in più sessioni. Se un utente utilizza la tua app su più dispositivi in un solo giorno, ogni dispositivo contribuirà al numero di utenti attivi per quel giorno. Se più utenti utilizzano lo stesso dispositivo in un solo giorno, viene conteggiato come un solo utente attivo.

La percentuale di arresti anomali percepiti dagli utenti è un fattore fondamentale, ovvero influisce sulla rilevabilità della tua app su Google Play. È importante perché gli arresti anomali che conta si verificano sempre quando l'utente interagisce con l'app, causando il maggior numero di interruzioni.

Google Play ha definito due soglie relative alle prestazioni scadenti per questa metrica:

  • Soglia generale di comportamenti dannosi: almeno l'1, 09% degli utenti attivi giornalieri ha riscontrato un arresto anomalo percepito dall'utente su tutti i modelli di dispositivi.
  • Soglia relativa alle prestazioni scadenti per dispositivo: almeno l'8% degli utenti attivi giornalieri ha riscontrato un arresto anomalo percepito dall'utente, su un singolo modello di dispositivo.

Se la tua app supera la soglia generale relativa alle prestazioni scadenti, è probabile che sia meno rilevabile su tutti i dispositivi. Se la tua app supera la soglia relativa alle prestazioni scadenti per dispositivo su alcuni dispositivi, è probabile che sia meno rilevabile su tali dispositivi e potrebbe essere mostrato un avviso nella tua scheda dello Store.

Android vitals può avvisarti tramite Play Console quando la tua app presenta arresti anomali eccessivi.

Per informazioni su come Google Play raccoglie i dati Android vitals, consulta la documentazione di Play Console.

Diagnostica gli arresti anomali

Dopo aver identificato che la tua app segnala arresti anomali, il passaggio successivo consiste nell'eseguire la diagnosi. Risolvere gli arresti anomali può essere difficile. Tuttavia, se riesci a identificare la causa principale dell'arresto anomalo, molto probabilmente riuscirai a trovare una soluzione.

Esistono molte situazioni che possono causare un arresto anomalo nella tua app. Alcuni motivi sono evidenti, ad esempio il controllo di un valore null o una stringa vuota, mentre altri sono più delicati, come il passaggio di argomenti non validi a un'API o anche interazioni multithread complesse.

Gli arresti anomali su Android producono un'analisi dello stack, ovvero uno snapshot della sequenza di funzioni nidificate chiamate nel programma fino al momento in cui si è verificato l'arresto anomalo. Puoi visualizzare le analisi dello stack in caso di arresto anomalo in Android vitals.

Come leggere un'analisi dello stack

Il primo passaggio per correggere un arresto anomalo è identificare il luogo in cui si verifica. Puoi utilizzare l'analisi dello stack disponibile nei dettagli del report se utilizzi Play Console o l'output dello strumento logcat. Se non è disponibile un'analisi dello stack, devi riprodurre in locale l'arresto anomalo, testando manualmente l'app o contattando gli utenti interessati, per poi riprodurlo durante l'utilizzo di logcat.

La traccia seguente mostra un esempio di arresto anomalo su un'app scritta utilizzando il linguaggio di programmazione 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

Un'analisi dello stack mostra due informazioni fondamentali per il debug di un arresto anomalo:

  • Il tipo di eccezione generata.
  • La sezione di codice in cui viene generata l'eccezione.

Il tipo di eccezione generata è di solito un chiaro indizio di cosa è andato storto. Verifica se si tratta di un elemento IOException, OutOfMemoryError o altro e trova la documentazione sulla classe dell'eccezione.

La classe, il metodo, il file e il numero di riga del file di origine in cui viene generata l'eccezione vengono mostrati sulla seconda riga di un'analisi dello stack. Per ogni funzione chiamata, un'altra riga mostra il sito della chiamata precedente (chiamato stack frame). Esaminando lo stack ed esaminando il codice, potresti trovare un punto che trasmette un valore errato. Se il codice non viene visualizzato nell'analisi dello stack, è probabile che tu abbia passato un parametro non valido in un'operazione asincrona. Spesso puoi capire cosa è successo esaminando ogni riga dell'analisi dello stack, trovando le classi API utilizzate e verificando che i parametri passati siano corretti e che li hai chiamati da una posizione consentita.

Le analisi dello stack per le app con codice C e C++ funzionano in modo simile.

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

Se non visualizzi informazioni a livello di classe e di funzione nelle analisi dello stack native, potresti dover generare un file di simboli di debug nativo e caricarlo in Google Play Console. Per maggiori informazioni, consulta Deoffuscare le analisi dello stack in caso di arresto anomalo. Per informazioni generali sugli arresti anomali nativi, consulta Diagnosi degli arresti anomali nativi.

Suggerimenti per riprodurre un arresto anomalo

È possibile che tu non riesca a riprodurre il problema semplicemente avviando un emulatore o collegando il dispositivo al computer. Gli ambienti di sviluppo tendono ad avere più risorse, ad esempio larghezza di banda, memoria e spazio di archiviazione. Utilizza il tipo di eccezione per determinare quale potrebbe essere la risorsa scarsa o trovare una correlazione tra la versione di Android, il tipo di dispositivo o la versione della tua app.

Errori di memoria

Se hai un OutOfMemoryError, potresti creare un emulatore con scarsa capacità di memoria per i test. La Figura 2 mostra le impostazioni del gestore di eventi di durata media in cui puoi controllare la quantità di memoria sul dispositivo.

Impostazione memoria in AVD Manager

Figura 2. Impostazione memoria in AVD Manager

Eccezioni di networking

Dal momento che gli utenti entrano ed escono spesso dalla copertura della rete mobile o Wi-Fi, le eccezioni di rete di un'applicazione di solito non devono essere trattate come errori, ma piuttosto come normali condizioni operative che si verificano inaspettatamente.

Se devi riprodurre un'eccezione di rete, ad esempio UnknownHostException, prova ad attivare la modalità aereo mentre l'applicazione tenta di utilizzare la rete.

Un'altra opzione consiste nel ridurre la qualità della rete nell'emulatore scegliendo un'emulazione della velocità di rete e/o un ritardo di rete. Puoi utilizzare le impostazioni Velocità e Latenza in Gestore AVD oppure puoi avviare l'emulatore con i flag -netdelay e -netspeed, come mostrato nel seguente esempio di riga di comando:

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

Questo esempio imposta un ritardo di 20 secondi per tutte le richieste di rete e una velocità di caricamento e download di 14,4 Kbps. Per ulteriori informazioni sulle opzioni della riga di comando per l'emulatore, vedi Avviare l'emulatore dalla riga di comando.

Lettura con logcat

Quando sei in grado di riprodurre i passaggi per riprodurre l'arresto anomalo, puoi utilizzare uno strumento come logcat per ottenere ulteriori informazioni.

L'output logcat mostrerà quali altri messaggi di log hai stampato, insieme ad altri messaggi del sistema. Non dimenticare di disattivare eventuali istruzioni Log aggiuntive che hai aggiunto perché la stampa comporta un consumo di CPU e batteria mentre l'app è in esecuzione.

Prevenzione degli arresti anomali causati da eccezioni di puntatore null

Le eccezioni di puntatore null (identificate dal tipo di errore di runtime NullPointerException) si verificano quando tenti di accedere a un oggetto nullo, in genere richiamando i suoi metodi o accedendo ai suoi membri. Le eccezioni Null Point sono la causa principale degli arresti anomali dell'app su Google Play. Lo scopo di null è indicare che l'oggetto non è presente, ad esempio non è stato ancora creato o assegnato. Per evitare eccezioni di puntatore null, devi assicurarti che i riferimenti all'oggetto su cui stai lavorando siano non null prima di chiamare metodi su di essi o provare ad accedere ai relativi membri. Se il riferimento all'oggetto è nullo, gestisci bene questo caso (ad esempio, esci da un metodo prima di eseguire qualsiasi operazione sul riferimento all'oggetto e scrivi informazioni in un log di debug).

Poiché non vuoi avere controlli null per ogni parametro di ogni metodo chiamato, puoi fare affidamento sull'IDE o sul tipo di oggetto per indicare la nulla.

Linguaggio di programmazione Java

Le seguenti sezioni riguardano il linguaggio di programmazione Java.

Avvisi relativi al tempo di compilazione

Annota i parametri dei tuoi metodi e restituisci i valori con @Nullable e @NonNull per ricevere avvisi relativi al tempo di compilazione dall'IDE. Questi avvisi ti chiedono di aspettarti un oggetto null su cui è possibile assegnare valori:

Avviso di eccezione del puntatore nullo

Questi controlli null riguardano oggetti che sai potrebbero essere nulli. Un'eccezione su un oggetto @NonNull è un'indicazione di un errore nel codice che deve essere risolto.

Errori in fase di compilazione

Poiché il valore di valori null deve essere significativo, puoi incorporarlo nei tipi che utilizzi, in modo che venga eseguito un controllo in fase di compilazione per verificare la presenza di un valore null. Se sai che un oggetto può essere nullo e che deve essere gestito nulla, puoi aggregarlo in un oggetto come Optional. Dovresti sempre preferire i tipi che trasmettono valori null.

Kotlin

In Kotlin, la nullabilità fa parte del sistema dei tipi. Ad esempio, una variabile deve essere dichiarata dall'inizio come null o non null. I tipi che supportano valori null sono contrassegnati con un ?:

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

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

Alle variabili non nulli non può essere assegnato un valore nullo e per le variabili con null è necessario verificare l'eventuale presenza di valori null prima di essere utilizzate come valori non null.

Se non vuoi verificare esplicitamente la presenza di null, puoi utilizzare l'operatore di chiamata sicura ?.:

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

Come best practice, assicurati di risolvere il problema delle maiuscole/minuscole per un oggetto nullo, altrimenti la tua app potrebbe entrare in stati imprevisti. Se l'applicazione non si arresta in modo anomalo con NullPointerException, non saprai che esistono questi errori.

Di seguito sono riportati alcuni modi per verificare la presenza di un valore nullo:

  • if controlli

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

    A causa dello smart-cast e del controllo null, il compilatore Kotlin sa che il valore della stringa è diverso da null, quindi ti consente di utilizzare il riferimento direttamente, senza la necessità dell'operatore di chiamata sicura.

  • ?: Operatore Elvis

    Questo operatore consente di specificare "se l'oggetto è diverso da null, restituisce l'oggetto, altrimenti restituisce qualcos'altro".

    val length = string?.length ?: 0
    

Puoi ancora ricevere un NullPointerException a Kotlin. Di seguito sono riportate le situazioni più comuni:

  • Quando generi esplicitamente un NullPointerException.
  • Quando utilizzi l'operatore !! per l'asserzione nulla. Questo operatore converte qualsiasi valore in un tipo non null, generando NullPointerException se il valore è null.
  • Quando si accede a un riferimento nullo di un tipo di piattaforma.

Tipi di piattaforma

I tipi di piattaforma sono dichiarazioni di oggetti provenienti da Java. Questi tipi vengono trattati in modo speciale. I controlli null non vengono applicati, pertanto la garanzia "non-null" è uguale a quella di Java. Quando accedi a un riferimento del tipo di piattaforma, Kotlin non crea errori in fase di compilazione, ma questi riferimenti possono causare errori di runtime. Vedi il seguente esempio dalla documentazione di 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 si basa sull'inferenza dei tipi quando un valore della piattaforma viene assegnato a una variabile Kotlin oppure puoi definire quale tipo aspettarsi. Il modo migliore per garantire che lo stato di nullità corretto di un riferimento proveniente da Java è utilizzare annotazioni con valore nulla (ad esempio @Nullable) nel codice Java. Il compilatore Kotlin rappresenterà questi riferimenti come tipi effettivi con valori null o non nulli, non come tipi di piattaforma.

Le API Java Jetpack sono state annotate con @Nullable o @NonNull secondo necessità e un approccio simile è stato adottato nell'SDK Android 11. I tipi provenienti da questo SDK e utilizzati in Kotlin verranno rappresentati come tipi corretti con valori nulli o con valori non null.

A causa del sistema di tipi di Kotlin, abbiamo notato che le app hanno una notevole riduzione di NullPointerException arresti anomali. Ad esempio, l'app Google Home ha registrato una riduzione del 30% degli arresti anomali causati da eccezioni con null point durante l'anno in cui ha eseguito la migrazione dello sviluppo di nuove funzionalità a Kotlin.