Zamrzání (visnet) — je stav, kdy mobilní aplikace přestane na delší dobu reagovat na jakékoli akce uživatele. Na rozdíl od lagů (zpomalení) a glitchů (nesprávné chování) zamrzání zcela blokuje UI: dotyky nejsou zpracovávány, animace se zastaví, obrazovka „zamrzne". Příčinou je blokování hlavního vlákna synchronní operací, deadlock ve vícevláknovém kódu nebo anomálně dlouhé garbage collection. Podle Apple Main Thread Checker Documentation souvisí více než 40 % crash hlášení na iOS s blokováním hlavního vlákna. Na Androidu vede obdobná situace k ANR — systémovému dialogu „Aplikace neodpovídá".
Hlavní body
Zamrzání (freeze, hang) v mobilní aplikaci — je stav, kdy aplikace přestane zpracovávat vstupní události a aktualizovat rozhraní po dobu několika sekund nebo déle. Technicky to znamená, že hlavní vlákno (main thread) je blokováno a nemůže provést další cyklus runneru.
Lag — zpoždění do 500 ms, při kterém uživatel zaznamená zpomalení, ale aplikace dále funguje. Zamrzání trvá od 1 sekundy do desítek sekund. ANR na Androidu — je zvláštní případ zamrzání, které trvalo déle než 5 sekund a bylo detekováno systémem. Ne každé zamrzání vede k ANR, ale každý ANR je systémem zdokumentované zamrzání.
Na Androidu zamrzání delší než 5 sekund vyvolá ANR dialog s návrhem na zavření aplikace. Na iOS má systém watchdog — pokud aplikace nereaguje na události do 10–20 sekund, Watchdog ukončí proces kódem 0x8badf00d (ate bad food). Uživatel vidí pouze náhlé zavření aplikace a návrat na hlavní obrazovku.
Každá operace, která trvá déle než 100 ms a je spuštěna v hlavním vlákně, potenciálně způsobuje zamrzání. Podívejme se na hlavní zdroje blokování.
Čtení velkého souboru, síťový požadavek bez asynchronnosti, ukládání dat do SharedPreferences synchronní metodou apply následovanou commit — všechny tyto operace blokují hlavní vlákno. Na Androidu může synchronní čtení souboru o velikosti 10 MB trvat 200–500 ms v závislosti na rychlosti flash paměti. Na iOS synchronní načítání URLSession bez completionHandler blokuje UI po dobu odezvy serveru.
Když dvě vlákna čekají na uvolnění prostředků, které drží navzájem, vzniká deadlock. V mobilních aplikacích typický scénář — vlákno A blokuje Lock1 a čeká na Lock2, zatímco vlákno B blokuje Lock2 a čeká na Lock1. Obě vlákna zamrznou navždy. Pokud je jedním z nich hlavní vlákno, aplikace zcela zamrzne.
Chyba v logice — například while(true) bez podmínky ukončení nebo rekurze bez základního případu — vede k nekonečnému provádění na hlavním vlákně. Android to detekuje prostřednictvím ANR po 5 sekundách, iOS — prostřednictvím Stackshot, který zaznamenává nekonečně se opakující zásobník volání.
Diagnostika zamrzání vyžaduje nástroje schopné zaznamenat stav všech vláken v okamžiku blokování.
Při každém ANR systém Android ukládá soubor /data/anr/traces.txt, který obsahuje dump zásobníku každého vlákna aplikace. Analýza tohoto souboru — hlavní diagnostická metoda: je třeba najít vlákno main a zjistit, na které metodě se zastavilo. Pokud zásobník končí Thread.sleep, InputStream.read nebo Lock.lock — příčina je nalezena.
Xcode při zamrznutí aplikace (signál SIGSTOP) může pořídit Stackshot — snímek zásobníků všech vláken. Zapněte ve schématu „Logging" → „Include Stackshot Logs". Při pádu s kódem 0x8badf00d extrahujte crash log z Devices & Simulators a najděte vlákno com.apple.main-thread se zamrzlým zásobníkem.
Main Thread Checker automaticky detekuje volání UIKit z vláken na pozadí během provozu aplikace. Zapněte jej ve schématu (Diagnostics → Main Thread Checker). Každé varování je potenciální příčinou zamrzání, zejména pokud se vyskytuje v uzavření completionHandler síťového požadavku.
Příklad detekce blokování pomocí StrictMode na Androidu:
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())
}
}
Odstranění zamrzání začíná přesunem všech potenciálně dlouhých operací do vláken na pozadí. Podívejme se na konkrétní techniky pro každou platformu.
Kotlin Coroutines s viewModelScope.launch(Dispatchers.IO) zaručují, že síťová operace nebo čtení z databáze probíhá ve vlákně na pozadí. Dispatchers.Main se používá pouze pro aktualizaci UI. Důležité: všechny suspend funkce musí být strukturované — podřízené korutiny se ruší při zrušení rodiče, čímž se zabraňuje úniku vláken.
Grand Central Dispatch s DispatchQueue.global(qos: .userInitiated) pro úlohy na pozadí a DispatchQueue.main.async pro aktualizaci UI — standardní vzor. Vyhněte se sync() na hlavní frontě — to je zaručený deadlock. Používejte async/await (Swift 5.5+) pro čitelnější asynchronní kód s automatickým návratem do hlavního vlákna přes MainActor.
Bloky synchronized v Kotlin a @synchronized ve Swift na hlavním vlákně jsou nebezpečné: pokud jiné vlákno již toto zamykání získalo, hlavní vlákno zamrzne v čekání. Používejte atomické typy (AtomicInteger, atomické vlastnosti ve Swift) nebo sekvenční fronty místo zámků.
Příklad asynchronního načítání dat s korutinami na Androidu:
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)
}
}
}
Systematické předcházení zamrzání pomáhá kombinace nástrojů, architektonických principů a procesů code review.
Nastavte StrictMode s penaltyDeath pro politiky vláken — to povede k okamžitému pádu aplikace při detekci síťového volání nebo diskového I/O v hlavním vlákně. Vývojář nebude moci problém ignorovat. V produkčním sestavení používejte penaltyLog pro sběr statistik bez pádů.
Na iOS zapněte Main Thread Checker v Debug schématu a nastavte CI na spouštění testů s touto možností. Pokud test obsahuje volání UIKit z vlákna na pozadí — musí selhat. To je jediný spolehlivý způsob, jak odhalit problém před odesláním do TestFlight.
Přidejte do procesu code review povinný bod: kontrola, že každé síťové volání, práce se soubory, databází nebo těžké výpočty probíhají ve vlákně na pozadí. Deadlock lze detekovat statickým analyzátorem: Infer od Facebooku a Thread Safety Checker od Xcode najdou potenciální blokování před spuštěním.
Často kladené otázky
ANR (Application Not Responding) — systémové oznámení Androidu, které se objeví při zamrznutí hlavního vlákna déle než 5 sekund. Zamrzání je širší pojem: jakékoli blokování UI libovolné délky. Na iOS neexistuje ANR, ale existuje Watchdog s timeoutem 10–20 sekund.
Soubor se nachází v /data/anr/traces.txt. Pro přístup je vyžadován root nebo adb shell: proveďte adb shell cat /data/anr/traces.txt \> traces.txt s root oprávněními. V zásobníku najděte vlákno „main" — poslední volaná metoda ukazuje na příčinu blokování.
Pokud zamrzání trvá méně než 10 sekund, Watchdog se neaktivuje a aplikace prostě „zamrzne" do dokončení blokující operace. Uživatel nevidí pád, ale zažívá frustraci. Pro detekci takových případů použijte MetricKit s vlastním sledováním doby provádění.
Používejte UI testy s kontrolou, že se obrazovka otevře za méně než 1 sekundu. Přidejte do CI měření času mezi dotykem a objevením další obrazovky. Na Androidu používejte Espresso s IdlingResource pro čekání na asynchronní operace. Na iOS XCTest s XCTWaiter pro kontrolu doby načítání.
SwiftUI samo o sobě nezpůsobuje zamrzání, ale složité výpočty ve vlastnosti body — ano. Pokud se body počítá 500 ms kvůli těžkým operacím, UI zamrzne. Řešení — přesuňte výpočty do Task.detached a aktualizujte @State asynchronně na hlavním actorovi.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také