Замръзване (виснет) — е състояние, при което мобилното приложение спира да реагира на каквито и да било действия на потребителя за продължително време. За разлика от лаговете (забавяне) и гличовете (некоректно поведение), замръзването напълно блокира UI: докосванията не се обработват, анимацията спира, екранът „замръзва". Причината — блокиране на главната нишка от синхронна операция, deadlock в многопоточен код или аномално дълго събиране на боклук. Според Apple Main Thread Checker Documentation, повече от 40% от crash докладите на iOS са свързани с блокиране на главната нишка. На Android аналогична ситуация води до ANR — системен диалог „Приложението не отговаря".
Основни точки
Замръзване (freeze, hang) в мобилно приложение — е състояние, при което приложението спира да обработва входни събития и да обновява интерфейса за няколко секунди или повече. Технически това означава, че главната нишка (main thread) е блокирана и не може да изпълни следващия цикъл на runner-а.
Лаг — забавяне до 500 ms, при което потребителят забелязва забавяне, но приложението продължава да работи. Замръзване продължава от 1 секунда до десетки секунди. ANR на Android — е частен случай на замръзване, което е продължило повече от 5 секунди и е било открито от системата. Не всяко замръзване води до ANR, но всеки ANR е документирано от системата замръзване.
На Android замръзване над 5 секунди предизвиква ANR диалог с предложение за затваряне на приложението. На iOS системата има watchdog — ако приложението не реагира на събития в рамките на 10–20 секунди, Watchdog прекратява процеса с код 0x8badf00d (ate bad food). Потребителят вижда само внезапно затваряне на приложението и връщане към началния екран.
Всяка операция, която продължава повече от 100 ms и е пусната в главната нишка, потенциално причинява замръзване. Нека разгледаме основните източници на блокирания.
Четене на голям файл, мрежова заявка без асинхронност, записване на данни в SharedPreferences чрез синхронен метод apply, последван от commit — всички тези операции блокират главната нишка. На Android синхронното четене на файл от 10 MB може да отнеме 200–500 ms в зависимост от скоростта на флаш паметта. На iOS синхронното зареждане на URLSession без completionHandler блокира UI за времето на отговор от сървъра.
Когато две нишки чакат освобождаване на ресурси, държани една от друга, възниква deadlock. В мобилните приложения типичният сценарий — нишка A блокира Lock1 и чака Lock2, докато нишка B блокира Lock2 и чака Lock1. И двете нишки замръзват завинаги. Ако една от тях е главната нишка, приложението замръзва напълно.
Грешка в логиката — например while(true) без условие за изход или рекурсия без базов случай — води до безкрайно изпълнение на главната нишка. Android открива това чрез ANR след 5 секунди, iOS — чрез Stackshot, който записва безкрайно повтарящия се стек на извикванията.
Диагностиката на замръзване изисква инструменти, способни да запишат състоянието на всички нишки в момента на блокиране.
При всеки ANR системата Android запазва файл /data/anr/traces.txt, който съдържа дамп на стека на всяка нишка на приложението. Анализът на този файл — основният метод за диагностика: трябва да се намери нишката main и да се види на кой метод е спряла. Ако стекът завършва с Thread.sleep, InputStream.read или Lock.lock — причината е намерена.
Xcode при замръзване на приложението (сигнал SIGSTOP) може да направи Stackshot — моментна снимка на стековете на всички нишки. Включете в схемата „Logging" → „Include Stackshot Logs". При срив с код 0x8badf00d извлечете crash лога от Devices & Simulators и намерете нишката com.apple.main-thread със замръзнал стек.
Main Thread Checker автоматично открива извиквания на UIKit от фонови нишки по време на работа на приложението. Включете го в схемата (Diagnostics → Main Thread Checker). Всяко предупреждение е потенциална причина за замръзване, особено ако се появява в затварянето на completionHandler на мрежова заявка.
Пример за откриване на блокиране чрез StrictMode на 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())
}
}
Отстраняването на замръзване започва с преместване на всички потенциално дълги операции във фонови нишки. Нека разгледаме конкретни техники за всяка платформа.
Kotlin Coroutines с viewModelScope.launch(Dispatchers.IO) гарантират, че мрежовата операция или четенето от базата данни се изпълняват във фонова нишка. Dispatchers.Main се използва само за обновяване на UI. Важно: всички suspend функции трябва да бъдат структурирани — дъщерните корутини се отменят при отмяна на родителя, предотвратявайки изтичане на нишки.
Grand Central Dispatch с DispatchQueue.global(qos: .userInitiated) за фонови задачи и DispatchQueue.main.async за обновяване на UI — стандартният модел. Избягвайте sync() на главната опашка — това е гарантиран deadlock. Използвайте async/await (Swift 5.5+) за по-четим асинхронен код с автоматично връщане към главната нишка чрез MainActor.
Блоковете synchronized в Kotlin и @synchronized в Swift на главната нишка са опасни: ако друга нишка вече е хванала това заключване, главната нишка ще замръзне в очакване. Използвайте атомарни типове (AtomicInteger, атомарни свойства в Swift) или последователни опашки вместо заключвания.
Пример за асинхронно зареждане на данни с корутини на 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)
}
}
}
Предотвратяването на замръзване системно помага с комбинация от инструменти, архитектурни принципи и процеси на преглед на код.
Настройте StrictMode с penaltyDeath за политики на нишките — това ще доведе до незабавен срив на приложението при откриване на мрежово повикване или дисков вход-изход в главната нишка. Разработчикът няма да може да игнорира проблема. В продукционната версия използвайте penaltyLog за събиране на статистика без сривове.
На iOS включете Main Thread Checker в Debug схемата и настройте CI да пуска тестове с тази опция. Ако тестът съдържа извикване на UIKit от фонова нишка — трябва да се провали. Това е единственият надежден начин да се открие проблемът преди изпращане до TestFlight.
Добавете в процеса на преглед на код задължителна точка: проверка, че всяко мрежово повикване, работа с файлове, база данни или тежки изчисления се изпълняват във фонова нишка. Deadlock може да бъде открит чрез статичен анализатор: Infer от Facebook и Thread Safety Checker от Xcode намират потенциални блокирания преди изпълнение.
Често задавани въпроси
ANR (Application Not Responding) — системно известие на Android, което се появява при замръзване на главната нишка за повече от 5 секунди. Замръзване е по-широко понятие: всяко блокиране на UI с каквато и да е продължителност. На iOS няма ANR, но има Watchdog с таймаут от 10–20 секунди.
Файлът се намира в /data/anr/traces.txt. За достъп е необходим root или adb shell: изпълнете adb shell cat /data/anr/traces.txt \> traces.txt с root права. В стека намерете нишката „main" — последният извикан метод показва причината за блокирането.
Ако замръзването продължава по-малко от 10 секунди, Watchdog не се активира и приложението просто „виси" до завършване на блокиращата операция. Потребителят не вижда срив, но изпитва фрустрация. За откриване на такива случаи използвайте MetricKit с персонализирани проследявания на времето за изпълнение.
Използвайте UI тестове с проверка, че екранът се отваря за по-малко от 1 секунда. Добавете в CI измерване на времето между докосване и появяване на следващия екран. На Android използвайте Espresso с IdlingResource за изчакване на асинхронни операции. На iOS XCTest с XCTWaiter за проверка на времето за зареждане.
SwiftUI сам по себе си не причинява замръзване, но сложни изчисления в свойството body — да. Ако body се изчислява 500 ms поради тежки операции, UI замръзва. Решение — изнесете изчисленията в Task.detached и обновявайте @State асинхронно на главния актьор.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също