Bevriezen (visnet) — een toestand waarbij een mobiele app gedurende lange tijd niet reageert op gebruikersacties. In tegenstelling tot lag (vertraging) en glitches (incorrect gedrag), blokkeert bevriezing de UI volledig: aanrakingen worden niet verwerkt, animatie stopt, het scherm „bevriest". De oorzaak is blokkering van de hoofdthread door een synchrone bewerking, deadlock in multi-thread code of abnormaal lange garbage collection. Volgens Apple Main Thread Checker Documentation houdt meer dan 40% van de crashrapporten op iOS verband met blokkering van de hoofdthread. Op Android leidt een vergelijkbare situatie tot ANR — het systeemdialoogvenster „App reageert niet".
Belangrijkste punten
Bevriezen (freeze, hang) in een mobiele app — een toestand waarbij de app gedurende enkele seconden of langer stopt met het verwerken van invoergebeurtenissen en het bijwerken van de interface. Technisch betekent dit dat de hoofdthread (main thread) is geblokkeerd en de volgende runner-cyclus niet kan uitvoeren.
Lag — een vertraging tot 500 ms, waarbij de gebruiker traagheid opmerkt maar de app blijft werken. Bevriezing duurt van 1 seconde tot tientallen seconden. ANR op Android — een speciaal geval van bevriezing dat langer dan 5 seconden duurde en door het systeem werd gedetecteerd. Niet elke bevriezing leidt tot ANR, maar elke ANR is een door het systeem gedocumenteerde bevriezing.
Op Android veroorzaakt een bevriezing van meer dan 5 seconden een ANR-dialoogvenster met het voorstel de app te sluiten. Op iOS heeft het systeem een watchdog — als de app niet binnen 10–20 seconden op gebeurtenissen reageert, beëindigt Watchdog het proces met code 0x8badf00d (ate bad food). De gebruiker ziet alleen het plotseling sluiten van de app en terugkeer naar het startscherm.
Elke bewerking die langer dan 100 ms duurt en in de hoofdthread wordt gestart, kan mogelijk bevriezing veroorzaken. Laten we de belangrijkste bronnen van blokkering bekijken.
Het lezen van een groot bestand, een netwerkverzoek zonder asynchroniteit, het opslaan van gegevens in SharedPreferences via de synchrone methode apply gevolgd door commit — al deze bewerkingen blokkeren de hoofdthread. Op Android kan het synchroon lezen van een bestand van 10 MB 200–500 ms duren, afhankelijk van de snelheid van het flashgeheugen. Op iOS blokkeert synchrone URLSession-lading zonder completionHandler de UI gedurende de serverreactietijd.
Wanneer twee threads wachten op het vrijgeven van bronnen die door elkaar worden vastgehouden, ontstaat een deadlock. In mobiele apps is het typische scenario — thread A blokkeert Lock1 en wacht op Lock2, terwijl thread B Lock2 blokkeert en wacht op Lock1. Beide threads bevriezen voor altijd. Als een van hen de hoofdthread is, bevriest de app volledig.
Een fout in de logica — bijvoorbeeld while(true) zonder uitgangsvoorwaarde of recursie zonder basisgeval — leidt tot oneindige uitvoering op de hoofdthread. Android detecteert dit via ANR na 5 seconden, iOS — via Stackshot, dat een oneindig herhalende call-stack vastlegt.
Diagnostiek van bevriezing vereist hulpmiddelen die de toestand van alle threads op het moment van blokkering kunnen vastleggen.
Bij elke ANR slaat het Android-systeem het bestand /data/anr/traces.txt op, met daarin een stackdump van elke thread van de app. Analyse van dit bestand — de belangrijkste diagnostische methode: zoek de main-thread en kijk op welke methode deze is gestopt. Als de stack eindigt op Thread.sleep, InputStream.read of Lock.lock — is de oorzaak gevonden.
Xcode kan bij het bevriezen van de app (signaal SIGSTOP) een Stackshot maken — een momentopname van de stacks van alle threads. Schakel in het schema „Logging" → „Include Stackshot Logs" in. Bij een crash met code 0x8badf00d haalt u het crashlog uit Devices & Simulators en zoekt u de thread com.apple.main-thread met de bevroren stack.
Main Thread Checker detecteert automatisch UIKit-aanroepen vanuit achtergrondthreads tijdens het gebruik van de app. Schakel het in het schema in (Diagnostics → Main Thread Checker). Elke waarschuwing is een mogelijke oorzaak van bevriezing, vooral als deze voorkomt in de afsluiting van een completionHandler van een netwerkverzoek.
Voorbeeld van detectie van blokkering via StrictMode op 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())
}
}
Het oplossen van bevriezing begint met het verplaatsen van alle potentieel lange bewerkingen naar achtergrondthreads. Laten we de specifieke technieken voor elk platform bekijken.
Kotlin Coroutines met viewModelScope.launch(Dispatchers.IO) garanderen dat een netwerkbewerking of database-lezing in een achtergrondthread wordt uitgevoerd. Dispatchers.Main wordt alleen gebruikt voor het bijwerken van de UI. Belangrijk: alle suspend-functies moeten gestructureerd zijn — onderliggende coroutines worden geannuleerd bij annulering van de ouder, waardoor threadlekken worden voorkomen.
Grand Central Dispatch met DispatchQueue.global(qos: .userInitiated) voor achtergrondtaken en DispatchQueue.main.async voor het bijwerken van de UI — het standaardpatroon. Vermijd sync() op de hoofdwachtrij — dit is een gegarandeerde deadlock. Gebruik async/await (Swift 5.5+) voor beter leesbare asynchrone code met automatische terugkeer naar de hoofdthread via MainActor.
synchronized-blokken in Kotlin en @synchronized in Swift op de hoofdthread zijn gevaarlijk: als een andere thread deze blokkering al heeft verkregen, zal de hoofdthread bevriezen in afwachting. Gebruik atomaire types (AtomicInteger, atomaire eigenschappen in Swift) of sequentiële wachtrijen in plaats van blokkeringen.
Voorbeeld van asynchroon laden van gegevens met coroutines op 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)
}
}
}
Het systematisch voorkomen van bevriezing helpt met een combinatie van hulpmiddelen, architectuurprincipes en code-reviewprocessen.
Configureer StrictMode met penaltyDeath voor threadbeleid — dit leidt tot onmiddellijke crash van de app bij detectie van een netwerkoproep of schijf-I/O op de hoofdthread. De ontwikkelaar kan het probleem niet negeren. Gebruik in de productiebuild penaltyLog voor het verzamelen van statistieken zonder crashes.
Schakel op iOS Main Thread Checker in het Debug-schema in en configureer CI om tests met deze optie uit te voeren. Als de test een UIKit-aanroep vanuit een achtergrondthread bevat, moet deze mislukken. Dit is de enige betrouwbare manier om het probleem te detecteren voordat het naar TestFlight wordt gestuurd.
Voeg aan het code-reviewproces een verplicht punt toe: controleer of elke netwerkoproep, bestandsbewerking, database-query of zware berekening in een achtergrondthread wordt uitgevoerd. Deadlock kan worden gedetecteerd met een statische analyser: Infer van Facebook en Thread Safety Checker van Xcode vinden potentiële blokkeringen vóór uitvoering.
Veelgestelde vragen
ANR (Application Not Responding) — een systeemmelding van Android die verschijnt wanneer de hoofdthread langer dan 5 seconden bevriest. Bevriezing is een breder begrip: elke UI-blokkering van elke duur. Op iOS bestaat ANR niet, maar er is een Watchdog met een time-out van 10–20 seconden.
Het bestand bevindt zich in /data/anr/traces.txt. Voor toegang is root of adb shell vereist: voer adb shell cat /data/anr/traces.txt \> traces.txt uit met root-rechten. Zoek in de stack naar de thread „main" — de laatst aangeroepen methode wijst op de oorzaak van de blokkering.
Als de bevriezing minder dan 10 seconden duurt, wordt Watchdog niet geactiveerd en blijft de app gewoon „bevroren" tot de blokkerende bewerking is voltooid. De gebruiker ziet geen crash, maar ervaart frustratie. Gebruik MetricKit met aangepaste uitvoeringstijd-tracings om dergelijke gevallen te detecteren.
Gebruik UI-tests met controle of het scherm in minder dan 1 seconde wordt geopend. Voeg in CI meting toe van de tijd tussen aanraking en het verschijnen van het volgende scherm. Gebruik op Android Espresso met IdlingResource om te wachten op asynchrone bewerkingen. Op iOS XCTest met XCTWaiter om de laadtijd te controleren.
SwiftUI veroorzaakt op zichzelf geen bevriezing, maar complexe berekeningen in de body-eigenschap — wel. Als body gedurende 500 ms wordt berekend vanwege zware bewerkingen, bevriest de UI. Oplossing — verplaats de berekeningen naar Task.detached en werk @State asynchroon bij op de hoofdactor.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook