Arresti anomali

Un'app per Android va in crash ogni volta che si verifica un'uscita imprevista causata da un'eccezione o un segnale non gestito. Un'app scritta in 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 segnale non gestito, ad esempio SIGSEGV.

Quando un'app va in arresto anomalo, Android termina il processo dell'app e mostra una finestra di dialogo per informare l'utente che l'app si è arrestata, come mostrato nella figura 1.

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

Un'app non deve essere in esecuzione in primo piano per arrestarsi in modo anomalo. Qualsiasi componente dell'app, anche quelli come i ricevitori di trasmissione o i fornitori di contenuti che vengono eseguiti in background, può causare l'arresto anomalo di un'app. Questi arresti anomali spesso confondono gli utenti perché non interagivano attivamente con la tua app.

Se la tua app si arresta in modo anomalo, puoi utilizzare le indicazioni riportate in questa pagina per diagnosticare e risolvere il problema.

Rilevare 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 utilizzare Android vitals per visualizzare i tassi di arresto anomalo della tua app.

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 un arresto anomalo di qualsiasi tipo.
  • Percentuale di arresti anomali percepita dall'utente:la percentuale di utenti attivi giornalieri che hanno riscontrato almeno un arresto anomalo mentre utilizzavano attivamente la tua app (un arresto anomalo percepito dall'utente). Un'app è considerata in uso attivo se mostra un'attività o esegue un servizio 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 solo 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 un singolo utente attivo.

La percentuale di arresti anomali percepita dagli utenti è una metrica vitals essenziale, ovvero influisce sulla rilevabilità della tua app su Google Play. È importante perché gli arresti anomali che conta si verificano sempre quando gli utenti interagiscono con l'app, causando così il maggior numero di interruzioni.

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

  • Soglia relativa alle prestazioni scadenti generali: almeno l'1, 09% degli utenti attivi giornalieri ha riscontrato un arresto anomalo percepito dall'utente su tutti i modelli di dispositivo.
  • Soglia relativa alle prestazioni scadenti del 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 complessiva 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 questi dispositivi e potrebbe essere mostrato un avviso nella tua scheda dello Store.

Android vitals può avvisarti in Play Console quando la tua app mostra un numero eccessivo di arresti anomali.

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

Diagnosticare gli arresti anomali

Una volta identificato che la tua app segnala arresti anomali, il passaggio successivo è diagnosticarli. Risolvere i problemi relativi ai blocchi può essere difficile. Tuttavia, se riesci a identificare la causa principale dell'arresto anomalo, molto probabilmente potrai trovare una soluzione.

Esistono molte situazioni che possono causare un arresto anomalo nell'app. Alcuni motivi sono ovvi, come il controllo di un valore nullo o di una stringa vuota, ma altri sono più sottili, come il passaggio di argomenti non validi a un'API o anche interazioni multithread complesse.

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

Come leggere un'analisi dello stack

Il primo passo per risolvere un arresto anomalo è identificare il punto 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 l'arresto anomalo a livello locale, testando manualmente l'app o contattando gli utenti interessati, e riprodurlo utilizzando logcat.

La seguente traccia mostra un esempio di arresto anomalo di un'app scritta utilizzando Jetpack Compose:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException
    at com.android.developer.crashsample.ComposableSingletons$MainActivityKt.lambda$0(MainActivity.kt:27)
    at androidx.compose.foundation.ClickableNode.handleUpEvent(Clickable.kt:958)
    at androidx.compose.foundation.ClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:895)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:446)
    at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:181)
    at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:118)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:2650)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2969)
    at android.app.Activity.dispatchTouchEvent(Activity.java:4683)
    at android.os.Looper.loop(Looper.java:398)
    at android.app.ActivityThread.main(ActivityThread.java:9569)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:918)

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 fornisce un indizio molto forte su cosa è andato storto. Controlla se si tratta di un IOException, di un OutOfMemoryError o di qualcos'altro e trova la documentazione sulla classe di eccezione.

La classe, il metodo, il file e il numero di riga del file di origine in cui viene generata l'eccezione vengono visualizzati nella seconda riga di una traccia dello stack. Per ogni funzione chiamata, un'altra riga mostra il sito di chiamata precedente (chiamato frame di stack).

Esaminando il codice, potresti trovare un punto in cui viene passato un valore errato. Se il tuo codice non viene visualizzato nell'analisi dello stack, è probabile che da qualche parte tu abbia passato un parametro non valido a un'operazione asincrona. Spesso puoi capire cosa è successo esaminando ogni riga della trace dello stack, trovando le classi API che hai utilizzato e verificando che i parametri che hai passato siano corretti e che la chiamata sia stata effettuata da una posizione consentita.

Gli stack trace per le app con codice C e C++ funzionano in modo molto 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 vedi informazioni a livello di classe e funzione nelle analisi dello stack nativo, potresti dover generare un file di simboli di debug nativi e caricarlo su Google Play Console. Per saperne di più, vedi Deoffuscare le analisi dello stack degli arresti anomali. Per informazioni generali sugli arresti anomali nativi, vedi 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, come 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, puoi creare un emulatore con una capacità di memoria ridotta da utilizzare per i test. La figura 2 mostra le impostazioni di AVD Manager, dove puoi controllare la quantità di memoria sul dispositivo.

Impostazione della memoria in AVD Manager
Figura 2. Impostazione della memoria in AVD Manager.

Eccezioni di networking

Poiché gli utenti si spostano spesso all'interno e all'esterno della copertura di rete mobile o Wi-Fi, in un'applicazione le eccezioni di rete di solito non devono essere trattate come errori, ma piuttosto come normali condizioni operative che si verificano in modo imprevisto.

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

Un'altra opzione è ridurre la qualità della rete nell'emulatore scegliendo un'emulazione della velocità di rete, un ritardo di rete o entrambi. Puoi utilizzare le impostazioni Velocità e Latenza in AVD Manager oppure 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 su tutte le richieste di rete e una velocità di caricamento e download di 14,4 Kbps. Per saperne di più sulle opzioni della riga di comando per l'emulatore, vedi Avviare l'emulatore dalla riga di comando.

Lettura con logcat

Una volta che riesci a riprodurre l'arresto anomalo, puoi utilizzare uno strumento come logcat per ottenere maggiori informazioni.

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

Prevenire arresti anomali causati da eccezioni di puntatore nullo

Le eccezioni di puntatore nullo (identificate dal tipo di errore di runtime NullPointerException) si verificano quando tenti di accedere a un oggetto nullo, in genere richiamando i relativi metodi o accedendo ai relativi membri. Le eccezioni di puntatore nullo sono la principale causa di arresti anomali delle 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 nullo, devi assicurarti che i riferimenti agli oggetti con cui stai lavorando non siano nulli prima di chiamare i metodi o di tentare di accedere ai relativi membri. Se il riferimento all'oggetto è null, gestisci bene questo caso (ad esempio, esci da un metodo prima di eseguire qualsiasi operazione sul riferimento all'oggetto e scrivi le informazioni in un log di debug).

Poiché non vuoi eseguire controlli null per ogni parametro di ogni metodo chiamato, puoi fare affidamento sull'IDE o sul tipo di oggetto per indicare il supporto di valori null.

Kotlin

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

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

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

Alle variabili non annullabili non può essere assegnato un valore nullo e le variabili annullabili devono essere controllate per verificare il supporto di valori null prima di essere utilizzate come non nulle.

Se non vuoi controllare esplicitamente i valori 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 gestire il caso di valore nullo per un oggetto nullable, altrimenti la tua app potrebbe entrare in stati imprevisti. Se la tua applicazione non si arresta più con NullPointerException, non saprai che questi errori esistono.

Di seguito sono riportati alcuni modi per verificare la presenza di valori nulli:

  • if controlli

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

    Grazie allo smart cast e al controllo dei valori null, il compilatore Kotlin sa che il valore della stringa non è null, quindi ti consente di utilizzare direttamente il riferimento, senza la necessità dell'operatore di chiamata sicura.

  • ?: Operatore Elvis

    Questo operatore ti consente di dichiarare "se l'oggetto non è null, restituiscilo; altrimenti, restituisci qualcos'altro".

    val length = string?.length ?: 0
    

Puoi comunque ottenere un NullPointerException in Kotlin. Di seguito sono riportate le situazioni più comuni:

  • Quando lanci esplicitamente un NullPointerException.
  • Quando utilizzi l'operatore di asserzione null !!. 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 piattaforme sono dichiarazioni di oggetti provenienti da Java. Questi tipi sono trattati in modo speciale; i controlli null non sono applicati con la stessa rigidità, quindi la garanzia non null è la stessa di Java. Quando accedi a un riferimento di tipo piattaforma, Kotlin non crea errori di compilazione, ma questi riferimenti possono portare a errori di runtime. Vedi il seguente esempio tratto 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 del tipo quando un valore della piattaforma viene assegnato a una variabile Kotlin oppure puoi definire il tipo previsto. Il modo migliore per garantire lo stato di supporto di valori null corretto di un riferimento proveniente da Java è utilizzare le annotazioni di supporto di valori null (ad esempio @Nullable) nel codice Java. Il compilatore Kotlin rappresenterà questi riferimenti come tipi effettivi nullable o non nullable, non come tipi di piattaforma.

Le API Java Jetpack sono state annotate con @Nullable o @NonNull a seconda delle necessità e un approccio simile è stato adottato nell'SDK per Android 11. I tipi provenienti da questo SDK, utilizzati in Kotlin, verranno rappresentati come tipi corretti che accettano o meno valori null.

Il sistema di tipi di Kotlin riduce significativamente gli arresti anomali di NullPointerException. Ad esempio, l'app Google Home ha registrato una riduzione del 30% degli arresti anomali causati da eccezioni di puntatore nullo durante l'anno in cui è stata eseguita la migrazione dello sviluppo di nuove funzionalità a Kotlin.