Зависание (виснет) — это состояние, при котором мобильное приложение перестаёт реагировать на любые действия пользователя на продолжительное время. В отличие от лагов (замедление работы) и глюков (некорректное поведение), зависание полностью блокирует UI: касания не обрабатываются, анимация останавливается, экран «застывает». Причина — блокировка главного потока синхронной операцией, deadlock в многопоточном коде или аномально долгая сборка мусора. По данным Apple Main Thread Checker Documentation, более 40% crash-репортов iOS связаны с блокировкой главного потока. На Android аналогичная ситуация приводит к ANR — системному диалогу «Приложение не отвечает».
Главное
Зависание (freeze, hang) в мобильном приложении — это состояние, при котором приложение перестаёт обрабатывать события ввода и обновлять интерфейс на протяжении нескольких секунд и более. Технически это означает, что главный поток (main thread) заблокирован и не может выполнить очередной цикл раннера.
Лаг — это задержка до 500 мс, при которой пользователь замечает замедление, но приложение продолжает работать. Зависание длится от 1 секунды до десятков секунд. ANR на Android — это частный случай зависания, которое продлилось более 5 секунд и было обнаружено системой. Не каждое зависание приводит к ANR, но каждое ANR — это задокументированное системой зависание.
На Android зависание более 5 секунд вызывает ANR-диалог с предложением закрыть приложение. На iOS система имеет watchdog — если приложение не реагирует на события в течение 10–20 секунд, Watchdog завершает процесс с кодом 0x8badf00d (ate bad food). Пользователь видит лишь внезапное закрытие приложения на главный экран.
Любая операция, которая выполняется дольше 100 мс и запущена в главном потоке, потенциально вызывает зависание. Рассмотрим основные источники блокировок.
Чтение большого файла, сетевой запрос без асинхронности, сохранение данных в SharedPreferences синхронным методом apply с последующим commit — все эти операции блокируют главный поток. На Android синхронный чтение файла размером 10 МБ может занять 200–500 мс в зависимости от скорости флеш-памяти. На 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, atomic свойства в 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 не срабатывает, и приложение просто «висит» до завершения блокирующей операции. Пользователь не видит crash, но испытывает фрустрацию. Для обнаружения таких случаев используйте MetricKit с кастомными трейсами времени выполнения.
Используйте UI-тесты с проверкой того, что экран открывается за < 1 секунды. Добавьте в CI замер времени между tap и появлением следующего экрана. На Android используйте Espresso с IdlingResource для ожидания асинхронных операций. На iOS XCTest с XCTWaiter для проверки времени загрузки.
SwiftUI сам по себе не вызывает зависаний, но сложные вычисления в свойстве body — да. Если body вычисляется в течение 500 мс из-за тяжёлых операций, UI зависает. Решение — выгрузить вычисления в Task.detached и обновлять @State асинхронно на главном акторе.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также