Зависання в розробці — суть, причини та запобігання

Автор: IT Sectr Опубліковано: 2026-07-28 Час читання: 9 хв

Зависання (висне) — це стан, при якому мобільний застосунок перестає реагувати на будь-які дії користувача на тривалий час. На відміну від лагів (уповільнення роботи) і глюків (некоректна поведінка), зависання повністю блокує UI: дотики не обробляються, анімація зупиняється, екран «застигає». Причина — блокування головного потоку синхронною операцією, deadlock у багатопотоковому коді або аномально довге збирання сміття. За даними документації Apple Main Thread Checker, понад 40% звітів про збої 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) у мобільному застосунку — це стан, при якому застосунок перестає обробляти події введення та оновлювати інтерфейс протягом кількох секунд і більше. Технічно це означає, що головний потік заблоковано і він не може виконати черговий цикл раннера.

Відмінність зависання від лагу та ANR

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

Наслідки зависань

На Android зависання понад 5 секунд викликає ANR-діалог із пропозицією закрити застосунок. На iOS система має watchdog — якщо застосунок не реагує на події протягом 10–20 секунд, Watchdog завершує процес із кодом 0x8badf00d (ate bad food). Користувач бачить лише раптове закриття застосунку на головний екран.

Причини зависань на Android та iOS

Будь-яка операція, яка виконується довше 100 мс і запущена в головному потоці, потенційно викликає зависання. Розглянемо основні джерела блокувань.

Синхронне введення-виведення в UI-потоці

Читання великого файлу, мережевий запит без асинхронності, збереження даних у SharedPreferences синхронним методом apply з наступним commit — усі ці операції блокують головний потік. На Android синхронне читання файлу розміром 10 МБ може зайняти 200–500 мс залежно від швидкості флеш-пам'яті. На 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.

Peer Review з перевіркою багатопотоковості

Додайте в процес код-рев'ю обов'язковий пункт: перевірка, що будь-який мережевий виклик, робота з файлами, БД або важкі обчислення виконуються у фоновому потоці. 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 не спрацьовує, і застосунок просто «висить» до завершення блокуючої операції. Користувач не бачить crash, але відчуває фрустрацію. Для виявлення таких випадків використовуйте MetricKit з кастомними трейсами часу виконання.

Як тестувати застосунок на зависання?

Використовуйте UI-тести з перевіркою того, що екран відкривається за < 1 секунди. Додайте в CI замір часу між tap і появою наступного екрану. На Android використовуйте Espresso з IdlingResource для очікування асинхронних операцій. На iOS XCTest з XCTWaiter для перевірки часу завантаження.

Чи може SwiftUI викликати зависання?

SwiftUI сам по собі не викликає зависань, але складні обчислення у властивості body — так. Якщо body обчислюється протягом 500 мс через важкі операції, 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також