Зависање (виснет) — стање у којем мобилна апликација престаје да реагује на било које радње корисника на дужи временски период. За разлику од лагова (успоравање рада) и гличeва (неисправно понашање), зависање потпуно блокира UI: додири се не обрађују, анимација се зауставља, екран „замрзава". Узрок — блокирање главне нити синхроном операцијом, deadlock у вишенитном коду или аномално дуго прикупљање смећа. Према Apple Main Thread Checker Documentation, преко 40% crash извештаја на iOS-у је повезано са блокирањем главне нити. На Android-у аналогна ситуација доводи до ANR-а — системског дијалога „Апликација не одговара".
Главне тачке
Зависање (freeze, hang) у мобилној апликацији — стање у којем апликација престаје да обрађује догађаје уноса и ажурира интерфејс током неколико секунди или дуже. Технички, то значи да је главна нит (main thread) блокирана и не може да изврши следећи циклус рунера.
Лаг — кашњење до 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође