Blocco (congelamento) è uno stato in cui un'applicazione mobile smette di rispondere a qualsiasi azione dell'utente per un periodo prolungato. A differenza dei lag (rallentamento) e dei glitch (comportamento errato), il blocco blocca completamente l'UI: i tocchi non vengono elaborati, l'animazione si ferma, lo schermo si “congela”. La causa è il blocco del thread principale da un'operazione sincrona, un deadlock in codice multithread o una garbage collection anormalmente lunga. Secondo la Documentazione Apple Main Thread Checker, oltre il 40% dei crash report iOS è correlato al blocco del thread principale. Su Android, una situazione simile porta ad ANR — il dialogo di sistema “L'applicazione non risponde”.
Punti chiave
Blocco (congelamento) in un'applicazione mobile è uno stato in cui l'app smette di elaborare eventi di input e aggiornare l'interfaccia per diversi secondi o più. Tecnicamente, ciò significa che il thread principale è bloccato e non può eseguire la successiva iterazione del ciclo di esecuzione.
Un lag è un ritardo fino a 500 ms in cui l'utente nota un rallentamento ma l'app continua a funzionare. Il blocco dura da 1 secondo a decine di secondi. ANR su Android è un caso speciale di blocco che è durato più di 5 secondi ed è stato rilevato dal sistema. Non tutti i blocchi portano ad ANR, ma ogni ANR è un blocco documentato dal sistema.
Su Android, un blocco di oltre 5 secondi attiva un dialogo ANR che offre di chiudere l'app. Su iOS, il sistema ha un watchdog — se l'app non risponde agli eventi per 10–20 secondi, il Watchdog termina il processo con il codice 0x8badf00d (ate bad food). L'utente vede solo l'app chiudersi improvvisamente verso la schermata principale.
Qualsiasi operazione che richieda più di 100 ms e sia lanciata sul thread principale può potenzialmente causare un blocco. Vediamo le principali fonti di blocchi.
Leggere un file grande, una richiesta di rete senza asincronia, salvare dati in SharedPreferences con il metodo sincrono apply seguito da commit — tutte queste operazioni bloccano il thread principale. Su Android, leggere in modo sincrono un file di 10 MB può richiedere 200–500 ms a seconda della velocità della memoria flash. Su iOS, un caricamento URLSession sincrono senza completionHandler blocca l'UI per il tempo di risposta del server.
Quando due thread attendono il rilascio di risorse trattenute reciprocamente, si verifica un deadlock. Nelle applicazioni mobili, uno scenario tipico è che il thread A blocca Lock1 e aspetta Lock2, mentre il thread B blocca Lock2 e aspetta Lock1. Entrambi i thread si bloccano per sempre. Se uno di essi è il thread principale, l'applicazione si blocca completamente.
Un errore di logica — ad esempio, while(true) senza condizione di uscita o ricorsione senza caso base — porta a un'esecuzione infinita sul thread principale. Android lo rileva tramite ANR dopo 5 secondi, iOS — tramite Stackshot, che cattura uno stack di chiamate infinitamente ripetuto.
La diagnosi dei blocchi richiede strumenti in grado di catturare lo stato di tutti i thread al momento del blocco.
Ad ogni ANR, il sistema Android salva un file /data/anr/traces.txt contenente un dump dello stack di ogni thread dell'app. Analizzare questo file è il metodo principale di diagnosi: trovare il thread main e vedere su quale metodo si è fermato. Se lo stack termina con Thread.sleep, InputStream.read o Lock.lock — la causa è trovata.
Xcode può catturare uno Stackshot — un'istantanea degli stack di tutti i thread — quando l'applicazione si blocca (segnale SIGSTOP). Abilitare “Logging” → “Include Stackshot Logs” nello schema. In caso di crash con codice 0x8badf00d, estrarre il log di crash da Devices & Simulators e trovare il thread com.apple.main-thread con lo stack bloccato.
Main Thread Checker rileva automaticamente chiamate UIKit da thread in background durante l'esecuzione dell'app. Abilitarlo nello schema (Diagnostics → Main Thread Checker). Ogni avviso è una causa potenziale di blocco, specialmente se si verifica in una closure completionHandler di una richiesta di rete.
Esempio di rilevamento di blocchi tramite StrictMode su Android:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
Eliminare i blocchi inizia spostando tutte le operazioni potenzialmente lunghe su thread in background. Vediamo tecniche specifiche per ogni piattaforma.
Kotlin Coroutines con viewModelScope.launch(Dispatchers.IO) garantiscono che le operazioni di rete o le letture del database vengano eseguite in un thread in background. Dispatchers.Main viene utilizzato solo per gli aggiornamenti UI. Importante: tutte le funzioni suspend devono essere strutturate — le coroutine figlie vengono cancellate quando il genitore viene cancellato, prevenendo perdite di thread.
Grand Central Dispatch con DispatchQueue.global(qos: .userInitiated) per attività in background e DispatchQueue.main.async per aggiornamenti UI è il modello standard. Evitare sync() sulla coda principale — questo è un deadlock garantito. Usare async/await (Swift 5.5+) per codice asincrono più leggibile con ritorno automatico al thread principale tramite MainActor.
I blocchi synchronized in Kotlin e @synchronized in Swift sul thread principale sono pericolosi: se un altro thread ha già acquisito questo blocco, il thread principale si bloccherà in attesa. Utilizzare tipi atomici (AtomicInteger, proprietà atomiche in Swift) o code seriali invece dei blocchi.
Esempio di caricamento asincrono di dati con coroutine su Android:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
Una combinazione di strumenti, principi architetturali e processi di revisione del codice aiuta a prevenire sistematicamente i blocchi.
Configurare StrictMode con penaltyDeath per le politiche dei thread — ciò causerà un crash immediato dell'app quando viene rilevata una chiamata di rete o I/O su disco sul thread principale. Lo sviluppatore non può ignorare il problema. Nelle build di produzione, utilizzare penaltyLog per raccogliere statistiche senza crash.
Su iOS, abilitare Main Thread Checker nello schema Debug e configurare CI per eseguire test con questa opzione. Se un test contiene una chiamata UIKit da un thread in background — deve fallire. Questo è l'unico modo affidabile per identificare il problema prima dell'invio a TestFlight.
Aggiungere un punto obbligatorio al processo di revisione del codice: verificare che qualsiasi chiamata di rete, operazione su file, accesso al database o calcolo pesante venga eseguito in un thread in background. Il Deadlock può essere rilevato con analizzatori statici: Infer di Facebook e Thread Safety Checker di Xcode trovano blocchi potenziali prima dell'esecuzione.
Domande frequenti
ANR (Application Not Responding) è una notifica di sistema Android che appare quando il thread principale si blocca per più di 5 secondi. Il blocco è un concetto più ampio: qualsiasi blocco UI di qualsiasi durata. iOS non ha ANR, ma ha un Watchdog con un timeout di 10–20 secondi.
Il file si trova in /data/anr/traces.txt. L'accesso richiede privilegi di root o adb shell: eseguire adb shell cat /data/anr/traces.txt \> traces.txt con diritti di root. Nello stack, trovare il thread “main” — l'ultimo metodo chiamato indica la causa del blocco.
Se il blocco dura meno di 10 secondi, il Watchdog non si attiva e l'app semplicemente “rimane bloccata” fino al completamento dell'operazione bloccante. L'utente non vede un crash ma prova frustrazione. Per rilevare tali casi, utilizzare MetricKit con trace personalizzate del tempo di esecuzione.
Utilizzare test UI con verifica che lo schermo si apra in < 1 secondo. Aggiungere al CI la misurazione del tempo tra il tocco e la comparsa dello schermo successivo. Su Android, utilizzare Espresso con IdlingResource per attendere operazioni asincrone. Su iOS, utilizzare XCTest con XCTWaiter per verificare il tempo di caricamento.
SwiftUI stesso non causa blocchi, ma calcoli complessi nella proprietà body sì. Se il calcolo di body richiede 500 ms a causa di operazioni pesanti, l'UI si blocca. La soluzione è scaricare i calcoli in Task.detached e aggiornare @State in modo asincrono sull'attore principale.
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