Настройка отслеживания App Hang / UI Freeze в мобильном приложении
App Hang — это не крэш. Приложение живо, но не реагирует на касания. Пользователь видит застывший экран, тыкает в кнопку ещё раз, потом ещё — и уходит. В Crashlytics ничего нет, пользователь молчит, конверсия падает.
iOS Watchdog завершает процесс при hang > 4–8 секунд. Android генерирует ANR при > 5 секунд. Но зависания 200–500ms не вызывают системных событий — они просто убивают UX.
Откуда берутся зависания
На iOS самый частый источник коротких фризов — синхронный вызов на main thread в реакции на UI-событие:
// Антипаттерн: декодируем большой JSON на main thread
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductCell
// Если product.image — это Data, которую надо декодировать — это block main thread
cell.imageView?.image = UIImage(data: product.imageData)
return cell
}
UIImage(data:) синхронно декодирует JPEG/PNG. На iPhone SE с 1900x1200 изображением — 30–80ms блокировки main thread при каждом cellForRowAt.
На Android основной виновник Compose-экранов — лишние recomposition в LazyColumn:
// Антипаттерн: нестабильный тип в LazyColumn
@Composable
fun ProductList(products: List<Product>) { // List<> не стабильный тип
LazyColumn {
items(products) { product ->
ProductCard(product) // перерисовывается при любом изменении родителя
}
}
}
Замена List<Product> на ImmutableList<Product> (kotlinx.collections.immutable) или использование @Stable-аннотации на data class убирает лишние recomposition.
Инструменты для обнаружения
iOS — MetricKit Hang Diagnostics
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
payload.hangDiagnostics?.forEach { hang in
let duration = hang.hangDuration
let callStack = hang.callStackTree
// hang.hangDuration — MXAverage, не точное значение
// Минимальный трекинг для MetricKit: hang > 250ms
print("Hang duration: \(duration.averageMeasurement)")
}
}
}
MetricKit отдаёт данные с суточной задержкой. Для real-time нужно собственное решение.
iOS — Sentry App Hang Detection
SentrySDK.start { options in
options.dsn = "https://[email protected]/project"
options.enableAppHangTracking = true
options.appHangTimeoutInterval = 0.25 // 250ms — порог для репорта
}
Sentry запускает watchdog-поток, который пингует main thread каждые 100ms. Если ответа нет > appHangTimeoutInterval — снимает стек через backtrace_thread и отправляет как Issue.
Android — Jetpack Janky Frames
// FrameMetricsAggregator из AndroidX
val frameMetrics = FrameMetricsAggregator(FrameMetricsAggregator.JANK_DATA)
frameMetrics.add(activity)
// Позже:
val metrics = frameMetrics.metrics
metrics?.get(FrameMetricsAggregator.JANK_INDEX)?.let { jankArray ->
val jankyFrames = jankArray.size
// Количество фреймов > 16ms
}
Android — Android Profiler и Perfetto
В продакшене эти инструменты не используются, но для локальной диагностики — незаменимы. Android Profiler показывает System Trace: видно, где main thread занят, какие lock ожидаются, где GC-паузы.
Perfetto с android.view.Choreographer треком показывает dropped frames по экранам.
Настройка мониторинга в Datadog
// Трекинг Long Tasks в Datadog RUM
RUM.enable(with: RUM.Configuration(
applicationID: "your-rum-app-id",
longTaskThreshold: 0.1 // 100ms — всё, что дольше, попадает как Long Task
))
В Datadog дашборде строим виджет:
count:rum.long_task{env:production,service:ios-app}
group_by: @view.name
visualize_as: top_list
Это покажет, на каких экранах больше всего Long Tasks — и именно там копаем дальше.
Что делаем
- Подключаем Sentry
enableAppHangTrackingс порогом 250ms (iOS) - Настраиваем MetricKit subscriber для daily diagnostics
- Включаем Datadog RUM Long Task tracking с порогом 100ms
- На Android настраиваем
FrameMetricsAggregatorдля ключевых Activity - Строим дашборд по экранам с наибольшим числом Long Tasks
- Анализируем stack traces и выявляем конкретные виновники
Сроки
Базовая настройка через Sentry и Datadog: 4–8 часов. Полная диагностика с MetricKit и кастомными метриками по экранам: 1–2 дня. Стоимость рассчитывается индивидуально.







