Замръзване в разработката — същност, причини и предотвратяване

Автор: IT Sectr Публикувано: 2026-07-28 Време за четене: 9 мин

Замръзване (виснет) — е състояние, при което мобилното приложение спира да реагира на каквито и да било действия на потребителя за продължително време. За разлика от лаговете (забавяне) и гличовете (некоректно поведение), замръзването напълно блокира UI: докосванията не се обработват, анимацията спира, екранът „замръзва". Причината — блокиране на главната нишка от синхронна операция, deadlock в многопоточен код или аномално дълго събиране на боклук. Според Apple Main Thread Checker Documentation, повече от 40% от crash докладите на iOS са свързани с блокиране на главната нишка. На Android аналогична ситуация води до ANR — системен диалог „Приложението не отговаря".

Основни точки

  • Замръзване — пълно блокиране на UI за продължително време (секунди и десетки секунди), различно от лагове и гличове
  • Основни причини — блокиране на главната нишка от вход-изход, deadlock между нишки, безкраен цикъл и изтичане на памет с дълго GC
  • Диагностика включва Main Thread Checker на iOS, ANR логове /data/anr/traces.txt на Android и анализ на дампове на нишки
  • Отстраняване — преместване на всички потенциално дълги операции във фонов режим, използване на Structured Concurrency и избягване на synchronized в UI нишката
  • Превенция — StrictMode, Main Thread Checker в Debug схема, статичен анализ за deadlock и периодично пускане на тестове с измерване на времето за отговор

Какво е замръзване в мобилната разработка

Замръзване (freeze, hang) в мобилно приложение — е състояние, при което приложението спира да обработва входни събития и да обновява интерфейса за няколко секунди или повече. Технически това означава, че главната нишка (main thread) е блокирана и не може да изпълни следващия цикъл на runner-а.

Разлика между замръзване, лаг и ANR

Лаг — забавяне до 500 ms, при което потребителят забелязва забавяне, но приложението продължава да работи. Замръзване продължава от 1 секунда до десетки секунди. ANR на Android — е частен случай на замръзване, което е продължило повече от 5 секунди и е било открито от системата. Не всяко замръзване води до ANR, но всеки ANR е документирано от системата замръзване.

Последствия от замръзване

На Android замръзване над 5 секунди предизвиква ANR диалог с предложение за затваряне на приложението. На iOS системата има watchdog — ако приложението не реагира на събития в рамките на 10–20 секунди, Watchdog прекратява процеса с код 0x8badf00d (ate bad food). Потребителят вижда само внезапно затваряне на приложението и връщане към началния екран.

Причини за замръзване на Android и iOS

Всяка операция, която продължава повече от 100 ms и е пусната в главната нишка, потенциално причинява замръзване. Нека разгледаме основните източници на блокирания.

Синхронен вход-изход в UI нишката

Четене на голям файл, мрежова заявка без асинхронност, записване на данни в SharedPreferences чрез синхронен метод apply, последван от commit — всички тези операции блокират главната нишка. На Android синхронното четене на файл от 10 MB може да отнеме 200–500 ms в зависимост от скоростта на флаш паметта. На iOS синхронното зареждане на URLSession без completionHandler блокира UI за времето на отговор от сървъра.

Deadlock в многопоточен код

Когато две нишки чакат освобождаване на ресурси, държани една от друга, възниква deadlock. В мобилните приложения типичният сценарий — нишка A блокира Lock1 и чака Lock2, докато нишка B блокира Lock2 и чака Lock1. И двете нишки замръзват завинаги. Ако една от тях е главната нишка, приложението замръзва напълно.

Безкраен цикъл или рекурсия

Грешка в логиката — например while(true) без условие за изход или рекурсия без базов случай — води до безкрайно изпълнение на главната нишка. Android открива това чрез ANR след 5 секунди, iOS — чрез Stackshot, който записва безкрайно повтарящия се стек на извикванията.

  • Android — Cursor без затваряне, синхронна заявка чрез execute() вместо enqueue(), FileInputStream.read() в UI нишката
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES, стартиране на NSURLConnection sendSynchronousRequest, зареждане на изображение с dataWithContentsOfURL
  • Крос-платформа — Flutter compute без отделен isolate, React Native синхронен NativeModule

Как да диагностицираме замръзване

Диагностиката на замръзване изисква инструменти, способни да запишат състоянието на всички нишки в момента на блокиране.

ANR логове на Android

При всеки ANR системата Android запазва файл /data/anr/traces.txt, който съдържа дамп на стека на всяка нишка на приложението. Анализът на този файл — основният метод за диагностика: трябва да се намери нишката main и да се види на кой метод е спряла. Ако стекът завършва с Thread.sleep, InputStream.read или Lock.lock — причината е намерена.

Stackshot на iOS

Xcode при замръзване на приложението (сигнал SIGSTOP) може да направи Stackshot — моментна снимка на стековете на всички нишки. Включете в схемата „Logging" → „Include Stackshot Logs". При срив с код 0x8badf00d извлечете crash лога от Devices & Simulators и намерете нишката com.apple.main-thread със замръзнал стек.

Main Thread Checker в Xcode

Main Thread Checker автоматично открива извиквания на UIKit от фонови нишки по време на работа на приложението. Включете го в схемата (Diagnostics → Main Thread Checker). Всяко предупреждение е потенциална причина за замръзване, особено ако се появява в затварянето на completionHandler на мрежова заявка.

Пример за откриване на блокиране чрез StrictMode на Android:

kotlin
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())
    }
}

Методи за отстраняване на блокирания на UI

Отстраняването на замръзване започва с преместване на всички потенциално дълги операции във фонови нишки. Нека разгледаме конкретни техники за всяка платформа.

Structured Concurrency с корутини

Kotlin Coroutines с viewModelScope.launch(Dispatchers.IO) гарантират, че мрежовата операция или четенето от базата данни се изпълняват във фонова нишка. Dispatchers.Main се използва само за обновяване на UI. Важно: всички suspend функции трябва да бъдат структурирани — дъщерните корутини се отменят при отмяна на родителя, предотвратявайки изтичане на нишки.

Асинхронни опашки на iOS

Grand Central Dispatch с DispatchQueue.global(qos: .userInitiated) за фонови задачи и DispatchQueue.main.async за обновяване на UI — стандартният модел. Избягвайте sync() на главната опашка — това е гарантиран deadlock. Използвайте async/await (Swift 5.5+) за по-четим асинхронен код с автоматично връщане към главната нишка чрез MainActor.

Избягване на synchronized в UI нишката

Блоковете synchronized в Kotlin и @synchronized в Swift на главната нишка са опасни: ако друга нишка вече е хванала това заключване, главната нишка ще замръзне в очакване. Използвайте атомарни типове (AtomicInteger, атомарни свойства в Swift) или последователни опашки вместо заключвания.

Пример за асинхронно зареждане на данни с корутини на Android:

kotlin
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

Настройте StrictMode с penaltyDeath за политики на нишките — това ще доведе до незабавен срив на приложението при откриване на мрежово повикване или дисков вход-изход в главната нишка. Разработчикът няма да може да игнорира проблема. В продукционната версия използвайте penaltyLog за събиране на статистика без сривове.

Main Thread Checker в Debug схема

На iOS включете Main Thread Checker в Debug схемата и настройте CI да пуска тестове с тази опция. Ако тестът съдържа извикване на UIKit от фонова нишка — трябва да се провали. Това е единственият надежден начин да се открие проблемът преди изпращане до TestFlight.

Преглед на код с проверка на многопоточността

Добавете в процеса на преглед на код задължителна точка: проверка, че всяко мрежово повикване, работа с файлове, база данни или тежки изчисления се изпълняват във фонова нишка. Deadlock може да бъде открит чрез статичен анализатор: Infer от Facebook и Thread Safety Checker от Xcode намират потенциални блокирания преди изпълнение.

  • Android — StrictMode, Infer, Android Lint Multithread, Kotlin Coroutines с viewModelScope
  • iOS — Main Thread Checker, TSAN (Thread Sanitizer), Xcode Analyze, Swift async/await с MainActor
  • Крос-платформа — Flutter compute isolate, React Native interaction manager с requestAnimationFrame

Често задавани въпроси

Каква е разликата между замръзване и ANR?

ANR (Application Not Responding) — системно известие на Android, което се появява при замръзване на главната нишка за повече от 5 секунди. Замръзване е по-широко понятие: всяко блокиране на UI с каквато и да е продължителност. На iOS няма ANR, но има Watchdog с таймаут от 10–20 секунди.

Как да прочетем traces.txt на Android?

Файлът се намира в /data/anr/traces.txt. За достъп е необходим root или adb shell: изпълнете adb shell cat /data/anr/traces.txt \> traces.txt с root права. В стека намерете нишката „main" — последният извикан метод показва причината за блокирането.

Защо приложението замръзва на iOS, но не се срива?

Ако замръзването продължава по-малко от 10 секунди, Watchdog не се активира и приложението просто „виси" до завършване на блокиращата операция. Потребителят не вижда срив, но изпитва фрустрация. За откриване на такива случаи използвайте MetricKit с персонализирани проследявания на времето за изпълнение.

Как да тестваме приложението за замръзване?

Използвайте UI тестове с проверка, че екранът се отваря за по-малко от 1 секунда. Добавете в CI измерване на времето между докосване и появяване на следващия екран. На Android използвайте Espresso с IdlingResource за изчакване на асинхронни операции. На iOS XCTest с XCTWaiter за проверка на времето за зареждане.

Може ли SwiftUI да причини замръзване?

SwiftUI сам по себе си не причинява замръзване, но сложни изчисления в свойството body — да. Ако body се изчислява 500 ms поради тежки операции, UI замръзва. Решение — изнесете изчисленията в Task.detached и обновявайте @State асинхронно на главния актьор.

Обобщение

  • Замръзване — пълно блокиране на UI за секунди и десетки секунди, причинено от блокиране на главната нишка, deadlock или безкраен цикъл
  • Диагностика — /data/anr/traces.txt на Android, Stackshot и Main Thread Checker на iOS
  • Основни причини — синхронен вход-изход, deadlock между нишки, безкрайна рекурсия, дълго GC
  • Отстраняване — корутини с правилни диспечери, async/await с MainActor, преместване на всички IO операции във фонови нишки
  • Превенция — StrictMode с penaltyDeath, Main Thread Checker, статичен анализ на deadlock (Infer, TSAN)
  • На Android замръзване > 5 сек = ANR; на iOS > 10–20 сек = Watchdog crash (0x8badf00d)
  • Препоръка: включете Thread Sanitizer в Debug схемата и настройте CI за пускане на тестове с TSAN за откриване на data race и deadlock

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също