Crash — аварийно спиране на мобилното приложение поради необработено изключение или фатална системна грешка. Според данните на Firebase Crashlytics, около 2% от потребителите се сблъскват с течение всеки ден, а всеки срив намалява задържането с 10–20%. Разбирането на причините и методите за предотвратяване на течение е задължително умение за мобилния разработчик.
Основни точки
Crash — е аварийно спиране на приложението, причинено от необработено изключение или фатален системен сигнал, който не е бил обработен в кода на приложението. Когато системата или виртуалната машина (JVM, ART) открие фатално състояние — NullPointerException, IndexOutOfBoundsException, OutOfMemoryError — тя незабавно спира процеса и го премахва от паметта. Потребителят вижда внезапно затваряне на приложението без никакво системно уведомление за грешка. Според Google, приложения с процент crash-free под 99% губят до 20% от активните си потребители на месец.
На Android механизмът за обработка на сривове се различава от настолните системи. Вместо диалогов прозорец за отстраняване на грешки с stack trace, Android просто убива процеса и не запазва подробна информация. Събирането на информация за срива е задача на библиотеки на трети страни (Crashlytics, Sentry, Bugsnag), които прихващат изключения чрез Thread.setDefaultUncaughtExceptionHandler, преди процесът да бъде прекратен.
iOS използва подобен механизъм с NSException и Mach exceptions за обработка на фатални грешки. При необработено изключение системата прекратява приложението, а отчетът се запазва като .crash файл. Събирането на сривове на iOS изисква интеграция с Crashlytics или вграден отчет чрез Xcode Organizer.
Пет категории сривове покриват 90% от всички сривове в мобилните приложения. Разбирането на всеки тип помага за по-бързо диагностициране и отстраняване на проблеми в продукционна среда.
NullPointerException (NPE) — най-разпространеният тип срив във всички Java/Kotlin приложения. Възниква при опит за извикване на метод или достъп до поле на обект, който е null. Типични сценарии: неинициализирано поле на Activity при завъртане на екрана, null отговор от сървъра при десериализация на JSON, невнимателна навигация чрез адаптера на RecyclerView.
Kotlin решава проблема с NPE на езиково ниво чрез null-безопасни типове: String? не може да бъде използван без изрична проверка. Съвместимостта с Java и Reflection обаче все още създават рискове. Използвайте анотации @NonNull и @Nullable и включете strictNullChecks в инструментите за статичен анализ.
fun safeLength(text: String?): Int {
return text?.length ?: 0 // безопасна обработка на null
}
IndexOutOfBoundsException възниква при достъп до несъществуващ индекс на списък или масив. Чест сценарий: премахване на елемент от RecyclerView без синхронизация с адаптера, многонишкова модификация на ArrayList без заключване, неправилно изчисляване на позиция в ViewPager. ConcurrentModificationException — близък роднина при едновременно итериране и модифициране на колекции.
Използвайте CopyOnWriteArrayList за многонишков достъп или Lock-free колекции от java.util.concurrent. За синхронизация с UI приложете DiffUtil, който безопасно и ефективно изчислява разликата между стария и новия списък.
ClassCastException възниква при преобразуване на обект в несъвместим тип. В Android типични причини: неправилен тип ViewHolder в RecyclerView (различни типове клетки без коректен getItemViewType), неправилно преобразуване на Fragment при навигация, Serializable обекти с различни версии на класове.
Използвайте безопасно преобразуване на Kotlin чрез оператора as?, който връща null при несъвместимост на типове. В Java — проверка чрез instanceof преди преобразуване. За Parcelable обекти задължително декларирайте CREATOR във всеки клас.
IllegalStateException сигнализира за извикване на метод в неподходящо състояние на обекта. Типичен пример в Android — getSupportFragmentManager() след onSaveInstanceState, когато commit() на фрагмента не е позволен. Друг чест случай — извикване на dismiss() на вече затворен диалогов прозорец.
Проверявайте състоянието на жизнения цикъл преди операции с FragmentManager. Използвайте commitAllowingStateLoss() само когато сте сигурни, че загубата на състояние не е критична. В Kotlin създавайте DSL-подобни строители, които изключват неправилни състояния на ниво типове.
Native Crash възниква в роден C/C++ код при нарушаване на паметта: достъп чрез нулев указател, double-free, препълване на стека. В Android такива сривове се случват в NDK библиотеки, игрови двигатели (Unity, Unreal) и системни зависимости. Native Crash НЕ се прихваща от Thread.setDefaultUncaughtExceptionHandler — убива процеса моментално.
За диагностика на родни сривове използвайте minidump файлове (Breakpad) или Android tombstone. Firebase Crashlytics поддържа събиране на родни сривове чрез NDK SDK. На iOS подобен проблем се решава чрез PLCrashReporter.
Три инструмента доминират на пазара за репортиране на мобилни сривове. Всеки предоставя събиране на stack trace, агрегиране по версии на приложението и уведомления за нови сривове.
Crashlytics — най-популярният репортер за сривове за мобилни приложения, част от екосистемата Firebase. Той автоматично събира stack trace, данни за устройството, версия на операционната система и потребителски персонализирани ключове. Интеграцията отнема 10 минути чрез Firebase Console и Gradle Plugin. Crashlytics също поддържа реални логове (Logcat) и персонализирано проследяване.
FirebaseCrashlytics.getInstance()
.setCustomKey("current_screen", "ProfileFragment")
FirebaseCrashlytics.getInstance()
.log("User tapped login button")
Sentry — алтернатива на Crashlytics с по-гъвкава система за филтриране и поддръжка на 90+ платформи. За разлика от Firebase, Sentry предоставя самостоятелно хостван сървър (self-hosted) за компании със строги изисквания към данните. Sentry поддържа дистрибутивно проследяване, breadcrumbs и интеграция с CI/CD пайплайни.
Bugsnag се отличава с поддръжка на предупреждения, базирани на тежест: разделя сривовете на критични, грешки и предупреждения. AppCenter от Microsoft — безплатен инструмент с основна функционалност за малки проекти. И двата поддържат Android, iOS, React Native и Flutter.
Анализът на срив е процес на възстановяване на пълната картина на събитието. Stack trace показва само последната точка на отказ, но не дава контекст, който е довел до проблема. Професионалният подход включва четири етапа.
Първи етап — четене на stack trace. Определете класа, метода и реда код, където е възникнало изключението. Проследете веригата от повиквания от горната рамка към долната: последният ред в стека е мястото на срива, а горните редове са последователността от повиквания. Деобфускацията (ProGuard/R8 mapping) е задължителна за продукционни компилации.
Втори етап — контекст на устройството. Crashlytics показва модела на устройството, версията на операционната система, наличната памет и версията на приложението. Например, срив само на Samsung Galaxy S10 с Android 11 показва проблем с конкретна версия на One UI, а не обща кодова грешка.
Трети етап — възпроизвеждане на тестово устройство. Ако сривът не се възпроизвежда стабилно, попитайте потребителя за точни стъпки или използвайте Remote Config за регистриране преди проблемния участък код. AB тестването на поправката върху част от аудиторията помага да се потвърди решението.
Четвърти етап — наблюдение след поправката. След публикуване на поправката, наблюдавайте честотата на срива в продължение на 3–5 дни. Ако сривът напълно е изчезнал — поправката е работила. Ако честотата е намаляла, но не е стигнала до нула — съществува втори сценарий, който изисква отделен анализ.
Системен подход за предотвратяване на сривове включва инструменти за статичен анализ, задължително тестване на гранични случаи и правилно обработване на грешки на всички нива на приложението.
Detekt (Kotlin) и Lint (Android) откриват потенциални проблеми във фазата на компилиране: неизползвани променливи, потенциални NPE, неправилно използване на API. Включете тези инструменти в CI пайплайна с праг на грешки. Например, Detekt с конфигурация от 30+ предупреждения или каквото и да е error-blocking не пропуска компилацията.
Покритието на ключови сценарии на използване с модулни тестове е основна защита срещу регресионни сривове. Тествайте модели на данни, ViewModel и UseCase слоеве с гранични случаи: null стойности, празни списъци, невалиден JSON. UI тестове чрез Espresso или Compose Test покриват критични потоци: удостоверяване, плащане, onboarding.
Проектирайте приложението така, че повреда в един модул да не срива целия екран. Използвайте catch блокове на ниво ViewModel с връщане на резервно състояние: показване на placeholder вместо списък, кеширани данни при липса на мрежа, резервно изображение при грешка при зареждане. Това превръща потенциалния срив в контролиран UX сценарий.
Staged rollouts — стандартна практика на Google Play и App Store: нова версия се разпространява до 5%, след това до 20% и накрая до 100% от аудиторията с интервал от 1–3 дни. На всеки етап се наблюдава честотата на сривове: ако crash-free процентът падне под 99.5%, пускането спира автоматично. Firebase Remote Config позволява деактивиране на проблемни функции без публикуване на нова версия.
Renovate или Dependabot в CI автоматично проверяват библиотеки за известни уязвимости и критични грешки. Актуализирането на една зависимост може да елиминира цял клас сривове. Въпреки това, тествайте актуализациите в staging среда преди пускане в продукция — новата версия на библиотеката може да съдържа несъвместими промени.
Често задавани въпроси
Не. Част от сривовете се причиняват от фактори извън контрола на разработчика: системни грешки, хардуерни проблеми, несъвместимост на фърмуера. Целта е да се намали честотата до 0.1% и по-ниско, а останалите сривове да се минимизират по отношение на времето за реакция.
Репортерът за сривове събира stack trace, състояние на паметта и устройството в момента на срива. Аналитиката събира поведенчески данни на потребителя. Crashlytics комбинира двата подхода, предоставяйки контекст на срива заедно с персонализирани ключове на потребителя.
ProGuard и R8 обфусцират кода за защита на интелектуалната собственост. За деобфускация качете mapping файла в Crashlytics при публикуване. Без mapping файл, stack trace ще покаже a.a(), b.b() вместо реалните имена на класове и методи.
Чрез Thread.setDefaultUncaughtExceptionHandler на Android: библиотеката регистрира свой собствен манипулатор, който първи получава необработеното изключение, запазва данните и едва тогава прекратява процеса. На iOS се използва NSSetUncaughtExceptionHandler за NSException и Mach exception handler за сигнали.
Fatal — приложението е прекратено. Non-fatal (уловено изключение) — разработчикът е уловил изключението чрез try-catch, но това може да показва потенциален проблем. Crashlytics различава тези типове и позволява филтриране на non-fatal отделно, за да не задръства таблото.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също