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

Автор: 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) заблокирован и не может выполнить очередной цикл раннера.

Отличие зависания от лага и 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, atomic свойства в 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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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