Frysning (visnet) — är ett tillstånd där en mobilapp slutar reagera på användarens handlingar under en längre tid. Till skillnad från lag (fördröjning) och glitchar (felaktigt beteende) blockerar frysning UI:t helt: beröringar bearbetas inte, animation stannar, skärmen „fryser". Orsaken är blockering av huvudtråden av en synkron operation, deadlock i flertrådad kod eller onormalt lång garbage collection. Enligt Apple Main Thread Checker Documentation är över 40 % av crash-rapporterna på iOS kopplade till blockering av huvudtråden. På Android leder en liknande situation till ANR — systemdialogen „Appen svarar inte".
Huvudpunkter
Frysning (freeze, hang) i en mobilapp — är ett tillstånd där appen slutar bearbeta inmatningshändelser och uppdatera gränssnittet under flera sekunder eller längre. Tekniskt innebär detta att huvudtråden (main thread) är blockerad och inte kan utföra nästa runner-cykel.
Lag — en fördröjning på upp till 500 ms, där användaren märker långsamhet men appen fortsätter att fungera. Frysning varar från 1 sekund till tiotals sekunder. ANR på Android — är ett specialfall av frysning som varade längre än 5 sekunder och upptäcktes av systemet. Inte varje frysning leder till ANR, men varje ANR är en av systemet dokumenterad frysning.
På Android orsakar frysning längre än 5 sekunder en ANR-dialog med förslag att stänga appen. På iOS har systemet en watchdog — om appen inte reagerar på händelser inom 10–20 sekunder avslutar Watchdog processen med kod 0x8badf00d (ate bad food). Användaren ser bara att appen plötsligt stängs och återgår till startskärmen.
Varje operation som varar längre än 100 ms och körs i huvudtråden kan potentiellt orsaka frysning. Låt oss titta på huvudkällorna till blockering.
Läsning av en stor fil, nätverksbegäran utan asynkronitet, lagring av data i SharedPreferences via synkron metod apply följt av commit — alla dessa operationer blockerar huvudtråden. På Android kan synkron läsning av en 10 MB fil ta 200–500 ms beroende på flashminnets hastighet. På iOS blockerar synkron URLSession-laddning utan completionHandler UI:t under serverns svarstid.
När två trådar väntar på frigöring av resurser som hålls av varandra uppstår deadlock. I mobilappar är det typiska scenariot — tråd A blockerar Lock1 och väntar på Lock2, medan tråd B blockerar Lock2 och väntar på Lock1. Båda trådarna fryser för evigt. Om en av dem är huvudtråden fryser appen helt.
Ett logikfel — till exempel while(true) utan utgångsvillkor eller rekursion utan basfall — leder till oändlig exekvering på huvudtråden. Android upptäcker detta via ANR efter 5 sekunder, iOS — via Stackshot, som registrerar en oändligt upprepande anropsstack.
Diagnostik av frysning kräver verktyg som kan registrera tillståndet för alla trådar vid blockeringstillfället.
Vid varje ANR sparar Android-systemet filen /data/anr/traces.txt, som innehåller stackdump för varje tråd i appen. Analys av denna fil — den huvudsakliga diagnostiska metoden: hitta main-tråden och se vilken metod den stoppades på. Om stacken slutar med Thread.sleep, InputStream.read eller Lock.lock — är orsaken funnen.
Xcode kan vid frysning av appen (signal SIGSTOP) ta en Stackshot — en ögonblicksbild av stackarna för alla trådar. Aktivera i schemat „Logging" → „Include Stackshot Logs". Vid krasch med kod 0x8badf00d, extrahera crashloggen från Devices & Simulators och hitta tråden com.apple.main-thread med fryst stack.
Main Thread Checker upptäcker automatiskt UIKit-anrop från bakgrundstrådar under appens körning. Aktivera det i schemat (Diagnostics → Main Thread Checker). Varje varning är en potentiell orsak till frysning, särskilt om den förekommer i avslutningen av en completionHandler för en nätverksbegäran.
Exempel på detektering av blockering via StrictMode på 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())
}
}
Åtgärd av frysning börjar med att flytta alla potentiellt långa operationer till bakgrundstrådar. Låt oss titta på specifika tekniker för varje plattform.
Kotlin Coroutines med viewModelScope.launch(Dispatchers.IO) garanterar att nätverksoperationen eller databasläsningen körs i en bakgrundstråd. Dispatchers.Main används endast för att uppdatera UI. Viktigt: alla suspend-funktioner måste vara strukturerade — underordnade korutiner avbryts när föräldern avbryts, vilket förhindrar trådläckage.
Grand Central Dispatch med DispatchQueue.global(qos: .userInitiated) för bakgrundsuppgifter och DispatchQueue.main.async för UI-uppdateringar — standardmönstret. Undvik sync() på huvudkön — detta är en garanterad deadlock. Använd async/await (Swift 5.5+) för mer läsbar asynkron kod med automatisk återgång till huvudtråden via MainActor.
synchronized-block i Kotlin och @synchronized i Swift på huvudtråden är farliga: om en annan tråd redan har tagit detta lås kommer huvudtråden att frysa i väntan. Använd atomära typer (AtomicInteger, atomära egenskaper i Swift) eller sekventiella köer istället för lås.
Exempel på asynkron dataladdning med korutiner på 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)
}
}
}
Systematiskt förebyggande av frysning hjälper med en kombination av verktyg, arkitektoniska principer och kodgranskningsprocesser.
Konfigurera StrictMode med penaltyDeath för trådpolicyer — detta leder till omedelbar krasch av appen vid upptäckt av nätverksanrop eller disk-I/O på huvudtråden. Utvecklaren kan inte ignorera problemet. I produktionsbygget använd penaltyLog för insamling av statistik utan krascher.
På iOS, aktivera Main Thread Checker i Debug-schemat och konfigurera CI att köra tester med detta alternativ. Om testet innehåller ett UIKit-anrop från en bakgrundstråd — bör det misslyckas. Detta är det enda pålitliga sättet att upptäcka problemet innan sändning till TestFlight.
Lägg till i kodgranskningsprocessen en obligatorisk punkt: kontrollera att varje nätverksanrop, filarbete, databasfråga eller tung beräkning utförs i en bakgrundstråd. Deadlock kan upptäckas med en statisk analysator: Infer från Facebook och Thread Safety Checker från Xcode hittar potentiella blockeringar före körning.
Vanliga frågor
ANR (Application Not Responding) — är ett systemmeddelande från Android som visas när huvudtråden fryser i mer än 5 sekunder. Frysning är ett bredare begrepp: varje UI-blockering oavsett varaktighet. På iOS finns ingen ANR, men det finns en Watchdog med en timeout på 10–20 sekunder.
Filen finns i /data/anr/traces.txt. För åtkomst krävs root eller adb shell: utför adb shell cat /data/anr/traces.txt \> traces.txt med root-rättigheter. I stacken, hitta tråden „main" — den senast anropade metoden anger orsaken till blockeringen.
Om frysningen varar mindre än 10 sekunder aktiveras inte Watchdog, och appen „fryser" bara tills den blockerande operationen är klar. Användaren ser ingen krasch men upplever frustration. För att upptäcka sådana fall, använd MetricKit med anpassad spårning av exekveringstid.
Använd UI-tester med kontroll att skärmen öppnas på mindre än 1 sekund. Lägg till i CI mätning av tiden mellan beröring och nästa skärms visning. På Android, använd Espresso med IdlingResource för att vänta på asynkrona operationer. På iOS, XCTest med XCTWaiter för att kontrollera laddningstiden.
SwiftUI i sig orsakar inte frysning, men komplexa beräkningar i body-egenskapen — ja. Om body beräknas i 500 ms på grund av tunga operationer fryser UI:t. Lösning — flytta beräkningarna till Task.detached och uppdatera @State asynkront på huvudaktören.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också