Crash è una terminazione anomala di un'applicazione mobile dovuta a un'eccezione non gestita o a un errore fatale del sistema. Secondo Firebase Crashlytics, circa il 2% degli utenti riscontra crash quotidianamente e ogni crash riduce la retention del 10–20%. Comprendere le cause e i metodi di prevenzione dei crash è un'abilità essenziale per qualsiasi sviluppatore mobile.
Punti chiave
Crash è una terminazione anomala di un'applicazione causata da un'eccezione non gestita o da un segnale fatale del sistema che non è stato gestito nel codice dell'applicazione. Quando il sistema o la macchina virtuale (JVM, ART) rileva una condizione fatale — NullPointerException, IndexOutOfBoundsException, OutOfMemoryError — interrompe immediatamente il processo e lo scarica dalla memoria. L'utente vede una chiusura improvvisa dell'app senza alcuna notifica di errore di sistema. Secondo Google, le app con un tasso crash-free inferiore al 99% perdono fino al 20% degli utenti attivi al mese.
Su Android, il meccanismo di gestione dei crash differisce dai sistemi desktop. Invece di un dialogo di debug con stack trace, Android semplicemente uccide il processo senza salvare informazioni dettagliate. La raccolta di informazioni sui crash è compito di librerie di terze parti (Crashlytics, Sentry, Bugsnag) che intercettano le eccezioni tramite Thread.setDefaultUncaughtExceptionHandler prima che il processo venga terminato.
iOS utilizza un meccanismo simile con NSException e Mach exceptions per gestire gli errori fatali. Quando si verifica un'eccezione non gestita, il sistema termina l'applicazione e il report viene salvato come file .crash. La raccolta di crash su iOS richiede l'integrazione con Crashlytics o il report integrato tramite Xcode Organizer.
Cinque categorie di crash coprono il 90% di tutti i fallimenti nelle applicazioni mobili. Comprendere ogni tipo aiuta a diagnosticare e risolvere i problemi in produzione più rapidamente.
NullPointerException (NPE) è il tipo di crash più comune in tutte le applicazioni Java/Kotlin. Si verifica quando si tenta di chiamare un metodo o accedere a un campo di un oggetto che è null. Scenari tipici: campo Activity non inizializzato durante la rotazione dello schermo, risposta null dal server durante la deserializzazione JSON, navigazione negligente attraverso l'adattatore RecyclerView.
Kotlin risolve il problema NPE a livello di linguaggio attraverso tipi null-safe: String? non può essere utilizzato senza un controllo esplicito. Tuttavia, la compatibilità Java e la Reflection creano ancora rischi. Utilizza le annotazioni @NonNull e @Nullable e attiva strictNullChecks negli strumenti di analisi statica.
fun safeLength(text: String?): Int {
return text?.length ?: 0 // gestione sicura di null
}
IndexOutOfBoundsException si verifica quando si accede a un indice inesistente di una lista o array. Scenari comuni: rimozione di un elemento da RecyclerView senza sincronizzazione con l'adattatore, modifica multi-thread di ArrayList senza blocco, calcolo errato della posizione in ViewPager. ConcurrentModificationException è un parente stretto quando si iterano e modificano le raccolte simultaneamente.
Utilizza CopyOnWriteArrayList per l'accesso multi-thread o raccolte senza blocco da java.util.concurrent. Per la sincronizzazione con l'UI, utilizza DiffUtil che calcola la differenza tra liste vecchia e nuova in modo sicuro ed efficiente.
ClassCastException si verifica quando si esegue il cast di un oggetto a un tipo incompatibile. In Android, le cause tipiche sono: tipo ViewHolder errato in RecyclerView (diversi tipi di cella senza un getItemViewType appropriato), cast errato di Fragment durante la navigazione, oggetti Serializable con versioni di classe diverse.
Utilizza il safe-cast di Kotlin tramite l'operatore as?, che restituisce null in caso di incompatibilità di tipo. In Java — verifica con instanceof prima del cast. Per gli oggetti Parcelable, dichiara sempre CREATOR in ogni classe.
IllegalStateException segnala la chiamata di un metodo in uno stato inappropriato dell'oggetto. Un esempio tipico in Android — getSupportFragmentManager() dopo onSaveInstanceState, quando commit() di un fragment non è consentito. Un altro caso comune — chiamare dismiss() su un dialogo già chiuso.
Verifica lo stato del ciclo di vita prima delle operazioni con FragmentManager. Utilizza commitAllowingStateLoss() solo quando sei sicuro che la perdita di stato non sia critica. In Kotlin, crea builder simili a DSL che eliminano stati non validi a livello di tipo.
Native Crash si verifica in codice nativo C/C++ a causa di violazioni di memoria: dereferenziazione di puntatore nullo, double-free, overflow del buffer di stack. In Android, questi crash si verificano in librerie NDK, motori di gioco (Unity, Unreal) e dipendenze di sistema. Native Crash NON viene intercettato da Thread.setDefaultUncaughtExceptionHandler — uccide il processo istantaneamente.
Per diagnosticare i crash nativi, utilizza file minidump (Breakpad) o tombstones Android. Firebase Crashlytics supporta la raccolta di crash nativi tramite NDK SDK. Su iOS, un problema simile viene risolto utilizzando PLCrashReporter.
Tre strumenti dominano il mercato del crash reporting mobile. Ognuno fornisce raccolta di stack trace, aggregazione per versione dell'app e notifiche di nuovi crash.
Crashlytics è il crash reporter più popolare per applicazioni mobili, parte dell'ecosistema Firebase. Raccoglie automaticamente stack trace, informazioni sul dispositivo, versione del sistema operativo e chiavi personalizzate dell'utente. L'integrazione richiede 10 minuti tramite Firebase Console e Gradle Plugin. Crashlytics supporta anche log in tempo reale (Logcat) e tracciamenti utente.
FirebaseCrashlytics.getInstance()
.setCustomKey("current_screen", "ProfileFragment")
FirebaseCrashlytics.getInstance()
.log("User tapped login button")
Sentry è un'alternativa a Crashlytics con un sistema di filtraggio più flessibile e supporto per oltre 90 piattaforme. A differenza di Firebase, Sentry fornisce un server auto-ospitato (self-hosted) per aziende con requisiti rigorosi sui dati. Sentry supporta il tracciamento distributivo, breadcrumbs e l'integrazione con pipeline CI/CD.
Bugsnag si distingue per il supporto di avvisi basati sulla gravità: classifica i crash in critical, error e warning. AppCenter di Microsoft è uno strumento gratuito con funzionalità di base per progetti piccoli. Entrambi supportano Android, iOS, React Native e Flutter.
L'analisi di un crash è il processo di ricostruzione del quadro completo di ciò che è accaduto. Lo stack trace mostra solo l'ultimo punto di fallimento ma non fornisce il contesto che ha portato al problema. Un approccio professionale include quattro fasi.
La prima fase — leggere lo stack trace. Identifica la classe, il metodo e la riga di codice in cui si è verificata l'eccezione. Segui la catena di chiamate dal frame superiore a quello inferiore: l'ultima riga nello stack è la posizione del crash e le righe superiori sono la sequenza di chiamate. La deoffuscazione (mapping ProGuard/R8) è obbligatoria per le build di produzione.
La seconda fase — il contesto del dispositivo. Crashlytics mostra il modello del dispositivo, la versione del sistema operativo, la memoria disponibile e la versione dell'app. Ad esempio, un crash solo su Samsung Galaxy S10 con Android 11 indica un problema con una versione specifica di One UI, non un errore generale del codice.
La terza fase — la riproduzione su un dispositivo di test. Se il crash non si riproduce in modo stabile, chiedi all'utente i passaggi esatti o utilizza Remote Config per registrare prima della sezione di codice problematica. Il test AB della correzione su parte del pubblico aiuta a confermare la soluzione.
La quarta fase — il monitoraggio post-correzione. Dopo aver pubblicato la correzione, monitora il tasso di crash per 3–5 giorni. Se il crash scompare completamente — la correzione ha funzionato. Se la frequenza è diminuita ma non è arrivata a zero — esiste un secondo scenario che richiede un'analisi separata.
Un approccio sistematico alla prevenzione dei crash include strumenti di analisi statica, test obbligatori dei casi limite e una corretta gestione degli errori a tutti i livelli dell'applicazione.
Detekt (Kotlin) e Lint (Android) trovano problemi potenziali al momento della compilazione: variabili inutilizzate, NPE potenziali, uso errato delle API. Includi questi strumenti nella pipeline CI con una soglia di errore. Ad esempio, Detekt con una configurazione di 30+ avvisi o qualsiasi blocco di errore non fa passare la build.
La copertura degli scenari d'uso chiave con test unitari è la protezione di base contro i crash di regressione. Testa modelli dati, ViewModel e livelli UseCase con casi limite: valori null, liste vuote, JSON non valido. I test dell'UI tramite Espresso o Compose Test coprono flussi critici: autenticazione, pagamento, onboarding.
Progetta l'applicazione in modo che un fallimento in un modulo non faccia crashare l'intero schermo. Utilizza blocchi catch a livello di ViewModel con stato di fallback: mostrare un placeholder invece di una lista, dati in cache quando offline, un'immagine di riserva in caso di errore di caricamento. Questo trasforma un potenziale crash in uno scenario UX controllato.
I rollout graduali sono una pratica standard su Google Play e App Store: una nuova versione viene distribuita al 5%, poi al 20% e poi al 100% del pubblico con intervalli di 1–3 giorni. In ogni fase, il tasso di crash viene monitorato: se il tasso crash-free scende sotto il 99,5%, il rollout si ferma automaticamente. Firebase Remote Config consente di disabilitare funzionalità problematiche senza pubblicare una nuova versione.
Renovate o Dependabot in CI controllano automaticamente le librerie per vulnerabilità note e bug critici. L'aggiornamento di una singola dipendenza può eliminare un'intera classe di crash. Tuttavia, testa gli aggiornamenti nell'ambiente di staging prima del rollout in produzione — una nuova versione della libreria potrebbe contenere modifiche incompatibili.
Domande frequenti
No. Alcuni crash sono causati da fattori fuori dal controllo dello sviluppatore: errori di sistema, problemi hardware, incompatibilità del firmware. L'obiettivo è ridurre il tasso allo 0,1% o inferiore e minimizzare il tempo di risposta per i crash rimanenti.
Un crash reporter raccoglie stack trace, stato della memoria e informazioni del dispositivo al momento del crash. L'analitica raccoglie dati comportamentali dell'utente. Crashlytics combina entrambi gli approcci, fornendo il contesto del crash insieme alle chiavi personalizzate dell'utente.
ProGuard e R8 offuscano il codice per proteggere la proprietà intellettuale. Per la deoffuscazione, carica il file di mapping in Crashlytics durante la pubblicazione. Senza il file di mapping, lo stack trace mostrerà a.a(), b.b() invece dei nomi reali di classi e metodi.
Tramite Thread.setDefaultUncaughtExceptionHandler su Android: la libreria registra il proprio gestore, che riceve per primo l'eccezione non gestita, salva i dati e solo allora termina il processo. Su iOS, NSSetUncaughtExceptionHandler viene utilizzato per NSException e Mach exception handler per i segnali.
Fatal — l'applicazione è terminata. Non fatal (eccezione catturata) — lo sviluppatore ha catturato l'eccezione tramite try-catch, ma potrebbe indicare un problema potenziale. Crashlytics distingue questi tipi e consente di filtrare i non fatal separatamente per non ingombrare la dashboard.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche