Lefagyás (visnet) — olyan állapot, amikor a mobilalkalmazás hosszabb időre abbahagyja a felhasználói műveletekre való reagálást. A lagektől (lassulás) és glitchektől (hibás viselkedés) eltérően a lefagyás teljesen blokkolja a UI-t: az érintések nem kerülnek feldolgozásra, az animáció megáll, a képernyő „befagy". Az ok — a fő szál blokkolása szinkron művelettel, deadlock a többszálú kódban vagy abnormálisan hosszú szemétgyűjtés. A Apple Main Thread Checker Documentation szerint az iOS crash jelentések több mint 40%-a a fő szál blokkolásához kapcsolódik. Androidon hasonló helyzet vezet az ANR-hez — „Az alkalmazás nem válaszol" rendszerpárbeszédablakhoz.
Főbb pontok
Lefagyás (freeze, hang) egy mobilalkalmazásban — olyan állapot, amikor az alkalmazás több másodpercre vagy tovább abbahagyja a bemeneti események feldolgozását és a felület frissítését. Technikailag ez azt jelenti, hogy a fő szál (main thread) blokkolva van, és nem tudja végrehajtani a következő runner ciklust.
A lag — legfeljebb 500 ms késleltetés, amikor a felhasználó lassulást észlel, de az alkalmazás tovább működik. Lefagyás 1 másodperctől több tíz másodpercig tart. Az ANR Androidon — a lefagyás speciális esete, amely több mint 5 másodpercig tartott és a rendszer észlelte. Nem minden lefagyás vezet ANR-hez, de minden ANR egy dokumentált lefagyás a rendszer által.
Androidon az 5 másodpercnél hosszabb lefagyás ANR párbeszédablakot vált ki az alkalmazás bezárására vonatkozó javaslattal. iOS-en a rendszer watchdoggal rendelkezik — ha az alkalmazás 10–20 másodpercen belül nem reagál az eseményekre, a Watchdog 0x8badf00d (ate bad food) kóddal befejezi a folyamatot. A felhasználó csak az alkalmazás hirtelen bezáródását és a kezdőképernyőre való visszatérést látja.
Minden olyan művelet, amely 100 ms-nál tovább tart és a fő szálban fut, potenciálisan lefagyást okoz. Vizsgáljuk meg a blokkolások fő forrásait.
Nagyméretű fájl olvasása, hálózati kérés aszinkronitás nélkül, adatok mentése SharedPreferences-be szinkron apply metódussal, majd commit — mindezen műveletek blokkolják a fő szálat. Androidon egy 10 MB-os fájl szinkron olvasása 200–500 ms-ig tarthat a flash memória sebességétől függően. iOS-en a URLSession szinkron betöltése completionHandler nélkül blokkolja a UI-t a szerver válaszának idejére.
Amikor két szál várja az egymás által tartott erőforrások felszabadítását, deadlock keletkezik. Mobilalkalmazásokban tipikus forgatókönyv — az A szál blokkolja a Lock1-et és várja a Lock2-t, míg a B szál blokkolja a Lock2-t és várja a Lock1-et. Mindkét szál örökre lefagy. Ha az egyik a fő szál, az alkalmazás teljesen lefagy.
Logikai hiba — például while(true) kilépési feltétel nélkül vagy rekurzió alapeset nélkül — végtelen végrehajtáshoz vezet a fő szálban. Android ezt ANR-en keresztül érzékeli 5 másodperc után, iOS — Stackshot-on keresztül, amely rögzíti a végtelenül ismétlődő hívási vermet.
A lefagyás diagnosztizálásához olyan eszközökre van szükség, amelyek képesek rögzíteni az összes szál állapotát a blokkolás pillanatában.
Minden ANR-nél az Android rendszer elmenti a /data/anr/traces.txt fájlt, amely az alkalmazás minden szálának veremdumpját tartalmazza. E fájl elemzése — a diagnosztika fő módszere: meg kell találni a main szálat, és megnézni, melyik metódusnál állt meg. Ha a verem Thread.sleep, InputStream.read vagy Lock.lock végződéssel zárul — az ok megtalálható.
Xcode az alkalmazás lefagyásakor (SIGSTOP jelzés) készíthet Stackshot-ot — az összes szál vermének pillanatképét. Kapcsolja be a sémában a „Logging" → „Include Stackshot Logs" lehetőséget. 0x8badf00d kóddal történő összeomláskor bontsa ki a crash naplót a Devices & Simulators menüből, és keresse meg a com.apple.main-thread szálat a lefagyott veremmel.
Main Thread Checker automatikusan észleli a UIKit hívásokat háttérszálakból az alkalmazás futása során. Kapcsolja be a sémában (Diagnostics → Main Thread Checker). Minden figyelmeztetés potenciális lefagyási ok, különösen, ha egy hálózati kérés completionHandler lezárásában fordul elő.
Példa a blokkolás észlelésére StrictMode-on keresztül Androidon:
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())
}
}
A lefagyás megszüntetése az összes potenciálisan hosszú művelet háttérszálakra való áthelyezésével kezdődik. Vizsgáljuk meg a platform-specifikus technikákat.
Kotlin Coroutines a viewModelScope.launch(Dispatchers.IO) segítségével garantálja, hogy a hálózati művelet vagy adatbázis olvasás háttérszálban történjen. A Dispatchers.Main csak a UI frissítésére szolgál. Fontos: minden suspend függvénynek strukturáltnak kell lennie — a gyermek korutinok a szülő lemondásakor lemondásra kerülnek, megelőzve a szálszivárgást.
Grand Central Dispatch a DispatchQueue.global(qos: .userInitiated) segítségével háttérfeladatokhoz és a DispatchQueue.main.async segítségével UI frissítésekhez — a szabványos minta. Kerülje a sync() használatát a fő soron — ez garantált deadlock. Használja az async/await (Swift 5.5+) funkciót az olvashatóbb aszinkron kódhoz, automatikus visszatéréssel a fő szálra MainActor segítségével.
A synchronized blokkok Kotlinben és a @synchronized Swiftben a fő szálon veszélyesek: ha egy másik szál már megszerezte ezt a zárolást, a fő szál lefagy a várakozásban. Használjon atomikus típusokat (AtomicInteger, atomikus tulajdonságok Swiftben) vagy szekvenciális sorokat a zárolások helyett.
Példa aszinkron adatbetöltésre korutinokkal Androidon:
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)
}
}
}
A lefagyás szisztematikus megelőzését eszközök, architektúraelvek és kódáttekintési folyamatok kombinációja segíti.
Állítsa be a StrictMode-ot penaltyDeath-tel a szálpolitikákhoz — ez az alkalmazás azonnali összeomlásához vezet, ha hálózati hívást vagy lemez I/O-t észlel a fő szálban. A fejlesztő nem hagyhatja figyelmen kívül a problémát. A production buildben használja a penaltyLog-ot a statisztikák összegyűjtéséhez összeomlás nélkül.
iOS-en kapcsolja be a Main Thread Checker-t a Debug sémában, és konfigurálja a CI-t, hogy ezzel az opcióval futtassa a teszteket. Ha a teszt UIKit hívást tartalmaz háttérszálból — meg kell hiúsulnia. Ez az egyetlen megbízható mód a probléma észlelésére a TestFlight-ba küldés előtt.
Adjon hozzá a kódáttekintési folyamathoz egy kötelező pontot: ellenőrizze, hogy minden hálózati hívás, fájlművelet, adatbázis-lekérdezés vagy nehéz számítás háttérszálban történik. A deadlock statikus elemzővel észlelhető: a Facebook Infer-e és az Xcode Thread Safety Checker-je potenciális blokkolásokat talál a futtatás előtt.
Gyakran ismételt kérdések
ANR (Application Not Responding) — egy Android rendszerértesítés, amely akkor jelenik meg, ha a fő szál több mint 5 másodpercre lefagy. A lefagyás tágabb fogalom: bármilyen UI blokkolás bármilyen időtartammal. iOS-en nincs ANR, de van Watchdog 10–20 másodperces időtúllépéssel.
A fájl a /data/anr/traces.txt útvonalon található. A hozzáféréshez root vagy adb shell szükséges: hajtsa végre a adb shell cat /data/anr/traces.txt \> traces.txt parancsot root jogosultságokkal. A veremben keresse meg a „main" szálat — az utoljára meghívott metódus jelzi a blokkolás okát.
Ha a lefagyás kevesebb mint 10 másodpercig tart, a Watchdog nem aktiválódik, és az alkalmazás egyszerűen „lefagy" a blokkoló művelet befejezéséig. A felhasználó nem lát összeomlást, de frusztrációt él át. Az ilyen esetek észleléséhez használja a MetricKit-et egyéni végrehajtási idő nyomkövetéssel.
Használjon UI teszteket annak ellenőrzésével, hogy a képernyő 1 másodpercen belül megnyílik. Adjon hozzá a CI-hez időmérést az érintés és a következő képernyő megjelenése között. Androidon használja az Espresso-t IdlingResource-szel az aszinkron műveletek várakozásához. iOS-en XCTest-et XCTWaiter-rel a betöltési idő ellenőrzéséhez.
A SwiftUI önmagában nem okoz lefagyást, de a body tulajdonságban végzett komplex számítások — igen. Ha a body 500 ms-ig számolódik nehéz műveletek miatt, a UI lefagy. Megoldás — helyezze át a számításokat a Task.detached-be, és frissítse a @State-et aszinkron módon a fő aktoron.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is