Загуба на връзка — едно от най-честите и дразнещи явления в мобилните приложения. Потребителят губи достъп до данни, операцията се прекъсва, приложението замръзва или крашва. Според Google Android Developer Blog, 70% от потребителите изтриват приложението, ако то крашне или замръзне два пъти. Нека разгледаме причините за загуба на връзка и начините за изграждане на устойчиви комуникации.
Основни точки
Прекъсва се — потребителски термин, описващ ситуация, когато приложението губи връзка със сървъра, спира да отговаря на действия или завършва с грешка. В технически смисъл това може да бъде: мрежова грешка (timeout, DNS failure), ANR (блокиране на UI нишка), crash (необработено изключение) или race condition (състояние на надпревара).
За потребителя всички тези сценарии изглеждат еднакво: приложението спира да работи. Разликата за разработчика е в подхода към диагностика и поправка. Мрежовите грешки се решават с retry механизми, ANR — с изнасяне на операции извън UI нишката, crash — с обработка на изключения.
Според Crittercism (сега Apteligent), средното мобилно приложение губи 1-2% от потребителите си при всяко крашване. За приложение с 1 милион потребители това са 10-20 хиляди загубени инсталации за един бъг. Това е особено критично за приложения във финансовия и медицинския сектор.
Нестабилна мрежа — мобилните устройства постоянно превключват между Wi-Fi и мобилна мрежа, влизат в зони без покритие (метро, асансьор, мазе). Всяко превключване причинява временна загуба на връзка, която приложението трябва да обработи правилно.
Таймаути — ако сървърът не отговори в определеното време (обикновено 10-30 секунди), клиентът хвърля SocketTimeoutException. Дългите таймаути без обратна връзка се възприемат от потребителя като замръзване. Препоръчва се настройка на таймаут не повече от 15 секунди.
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
Състояние на надпревара (race condition) — възниква, когато множество нишки едновременно четат и пишат едни и същи данни без синхронизация. Например, зареждане на данни от кеша в UI нишката паралелно с обновяване на кеша от мрежата може да доведе до показване на остарели или некоректни данни.
Offline-first — архитектурен модел, при който локалното хранилище (Room, CoreData) е единственият източник на истина. Мрежата се използва за синхронизация на данни във фонов режим. Потребителят винаги вижда актуални данни от локалния кеш, дори при липса на мрежа.
Repository pattern — единна входна точка за данни, която решава дали данните да се вземат от мрежата или от кеша. Репозиторието абстрахира източника на данни от ViewModel и UI. При мрежова грешка репозиторието автоматично превключва към локалния източник.
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): Result<List<User>> {
return try {
val remote = api.fetchUsers()
dao.insertAll(remote)
Result.success(remote)
} catch (e: IOException) {
val cached = dao.getAll()
if (cached.isNotEmpty()) {
Result.success(cached) // върни кеш при мрежова грешка
} else {
Result.failure(e)
}
}
}
}
Circuit Breaker — модел за защита на сървъра от лавина от заявки при недостъпност. След N последователни грешки прекъсвачът се отваря и всички заявки веднага връщат грешка без опит за свързване. След определен таймаут прекъсвачът преминава в полуотворено състояние за пробна заявка.
Exponential backoff — стандартен механизъм retry. След първия неуспех изчакайте 1 секунда, след втория — 2 секунди, след това 4, 8, 16. Ограничете максималния брой опити (обикновено 3-5), за да не претоварвате сървъра и батерията.
Обратна връзка с потребителя — при мрежова грешка покажете разбираемо съобщение: „Няма връзка”, „Сървърът е временно недостъпен”, „Проверете интернета”. Използвайте Snackbar или Inline State View. Никога не показвайте технически грешки (HTTP 500, SocketException) на потребителя.
ConnectivityManager — Android API за мониторинг на мрежата. Позволете на приложението да реагира на промени: при загуба на мрежа покажете placeholder, при възстановяване — автоматично обновете данните. В iOS използвайте NWPathMonitor от Network framework.
class NetworkMonitor(private val context: Context) {
private val manager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun isOnline(): Boolean {
val network = manager.activeNetwork ?: return false
val caps = manager.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
Crashlytics (Firebase) — стандартен инструмент за отчитане на крашвания за мобилни приложения. Събира stacktrace на всички необработени изключения, версия на ОС, модел на устройство и време на крашване. Позволява групиране на грешки и назначаване на отговорници за поправки.
Sentry — алтернатива на Crashlytics с поддръжка на мониторинг на производителността. Позволява проследяване на конкретни транзакции (напр. „автентикация на потребител”) и виждане на коя стъпка е възникнала грешката. Performance tracing помага да се разграничат мрежовите таймаути от бъгове в логиката на приложението.
Timber — библиотека за логване за Android с автоматично добавяне на тагове според класа. В debug сборка логвайте всички мрежови заявки и отговори. В release сборка — само грешки и предупреждения чрез Crashlytics.setCustomLog.
| Инструмент | Тип | Кога да използваме |
|---|---|---|
| Crashlytics | Crash reporting | Винаги в release — автоматично събиране на крашвания |
| Sentry | Crash + Performance | Когато трябва да профилирате конкретни потребителски сценарии |
| Timber | Logging | Debug: пълно логване; Release: само грешки |
| HTTP Toolkit | Network debug | Локално прихващане и анализ на HTTP трафик |
Според Firebase Summit 2023, приложенията, внедрили Crashlytics + Performance Monitoring, намаляват средното време за откриване и поправка на критични бъгове от 3 дни до 4 часа. Препоръчва се настройка на аларми за всяко крашване с честота над 0.1% от активните потребители.
Често задавани въпроси
Ако crash не се хваща в Crashlytics, проверете native crash (SIGSEGV, SIGABRT) — те не се обработват от Java/Kotlin exception handler. В Android това може да е изтичане на native памет от JNI, в iOS — EXC_BAD_ACCESS. Използвайте Breakpad (Android) или PLCrashReporter (iOS) за събиране на stacktrace на native crash.
Използвайте Network Link Conditioner (вграден в iOS, за Android има Facebook Network Connection Class или настройки Developer Options > Network > Select network type). Задайте закъснение 500-3000 ms и загуба на пакети 5-30%. Можете също да използвате Charles Proxy или mitmproxy за емулация на мрежови закъснения и прекъсвания.
ANR възниква, когато UI нишката е блокирана повече от 5 секунди. Мрежовите заявки трябва да се изпълняват във фонова нишка: coroutines (viewModelScope.launch(Dispatchers.IO)), RxJava (subscribeOn(Schedulers.io)) или WorkManager за синхронизация. Винаги задавайте таймаути на HTTP клиента — липсата на таймаут може да доведе до вечно блокиране.
Race condition — ситуация, при която резултатът от операция зависи от реда на изпълнение на нишките. Например, потребителят бързо натиска бутона „Изпрати” два пъти и заявката се изпраща два пъти. Решение: използвайте Mutex, single-threaded executors или state machine (деактивирайте бутона след първото натискане). В Kotlin използвайте Mutex от coroutines или анотацията @Synchronized.
Приложете Chaos Engineering за мобилни приложения: изключвайте мрежата по време на операции, симулирайте високо закъснение, превключвайте между Wi-Fi и мобилна мрежа, убивайте процеса от системата. Инструменти: Facebook Network Connection Class, Charles Proxy, iOS Network Link Conditioner. В CI/CD добавяйте UI тестове с различни мрежови условия чрез AndroidTest Orchestrator.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също