Забавя — това е потребителско описание на ситуация, когато мобилното приложение работи бавно и непостоянно: ту реагира нормално, ту внезапно замръзва за няколко секунди. В технически контекст „забавя“ означава комбинация от лагове и микро-замръзвания, причинени от чести GC паузи, блокиране на основната нишка от синхронни операции и неоптимални структури от данни. Според Android Performance Benchmarking Guide, намаляването на времето за отговор от 300 ms на 100 ms повишава задържането на потребителите с 25%. Диагностиката на забавяне изисква комбинация от CPU и Memory профилиране с анализ на честотата на събиране на отпадъци.
Основни точки
Забавя — неформален термин, с който потребителите описват субективно бавната работа на приложението. За разлика от лага, който се проявява като постоянно закъснение, забавянето е нерегулярно замръзване: приложението може да работи перфектно няколко секунди, а след това да се „замисли“ за 1–3 секунди.
От гледна точка на профилирането, забавя се проявява като серия от пропуснати кадри (jank) с пикови закъснения над 100 ms. На графиката на FPS това изглежда като резки спадове: 60 → 20 → 55 → 10 кадъра в секунда. За разлика от лага с равномерно нисък FPS, забавянето има изразена вариабилност.
Когато приложението забавя, потребителят не разбира логиката на забавянията: екранът може да се превърта плавно, а след това внезапно да спре за секунда. Това предизвиква фрустрация и намалява доверието в приложението. Според Google, 53% от потребителите напускат сайта или приложението, ако зареждането отнема повече от 3 секунди.
Непостоянният характер на забавянето показва, че проблемът е причинен от събитийни фактори, а не от постоянно претоварване. Нека разгледаме типичните сценарии.
На Android в среда ART събирането на отпадъци спира всички нишки на приложението. Ако в кода се създават много временни обекти — например при всяко извикване на onBindViewHolder се създава нов String чрез конкатенация — GC се стартира по-често. Паузата може да продължи 5–50 ms в зависимост от размера на купчината и поколението на обектите. Потребителят усеща това като внезапно „замисляне“.
Room на Android и Core Data на iOS поддържат асинхронни заявки, но разработчиците често извикват getValue() или изпълняват заявка чрез runBlocking за простота. Тежък SELECT с join-ове на таблица с 10 000 реда може да отнеме 200–500 ms, напълно блокирайки UI за това време.
Зареждането на изображение от камера (12 Mp, 4000x3000 px) без мащабиране отнема до 200 ms за декодиране в Bitmap. Ако изображенията се зареждат асинхронно, но без пул от нишки с ограничение, едновременното стартиране на 5–6 декодирания може да претовари CPU, причинявайки мигриращи забавяния.
Диагностиката на нерегулярни забавяния е по-трудна от диагностиката на постоянни лагове, тъй като проблемът може да не се възпроизвежда при всяко стартиране. Необходимо е събиране на статистика за по-дълъг период.
Android Studio Memory Profiler показва не само използването на паметта, но и GC събитията: честота, тип (Concurrent, Full), продължителност. Ако GC се случва по-често от 1 път на 5 секунди в спокойно състояние — това е признак на прекомерна алокация. Записването на heap dump в момента на забавяне позволява да се види кои обекти заемат паметта.
На iOS използвайте шаблона Allocations в Instruments за проследяване на създаването и освобождаването на обекти. Включете поколенията (Generations) — те позволяват да се правят моментни снимки на купчината между действията и да се види кои обекти остават в паметта. Постоянните обекти, които не се освобождават — източник на натрупване на памет и последващи паузи.
JankStats — библиотека на Android, която събира метрики на пропуснатите кадри в реално време. Тя свързва всеки jank с текущия сценарий (напр. „превъртане на списък“, „отваряне на екран“), което позволява да се разбере на кое конкретно действие възниква забавянето.
Пример за интеграция на JankStats за проследяване на замръзвания на Android:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Jank", "Duration=${frameData.durationMs}ms")
}
}
}
}
Премахването на забавянето изисква целенасочена работа с всяка причина. Няма универсално решение — необходим е анализ на конкретни профили на производителност.
Ако списъкът съдържа 1000+ елемента и всички се зареждат наведнъж — това е гарантирано забавяне. Paging 3 на Android и NSFetchedResultsController на iOS зареждат данни на порции по време на превъртане. Потребителят вижда само първите 10–20 елемента, останалите се зареждат на фона.
Room позволява профилиране на заявки чрез Inspection Tool в Android Studio: вижда се времето за изпълнение, броя на върнатите редове и плана на заявката. Добавянето на индекси към колоните WHERE и ORDER BY може да намали времето за заявка от 300 ms на 5 ms. На iOS аналогична проверка се извършва от Core Data Profiler в Instruments.
Фонови синхронизации, качване на файлове, обработка на данни — всичко това трябва да се изпълнява чрез WorkManager (Android) или Background Tasks (iOS). Ако синхронизацията се стартира в UI нишката, приложението ще забавя по време на изпълнение. WorkManager гарантира изпълнение във фонова нишка, като взема предвид състоянието на батерията и мрежата.
Пример за фонова синхронизация чрез WorkManager на Android:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Sync", "Синхронизиране на данни във фонова нишка")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
Забавянето може да бъде предотвратено на етапа на писане на код, като се следват принципите на ефективна работа с паметта и нишките.
Baseline Profiles — списък от класове и методи, които Android компилира предварително (AOT), а не JIT. Без профил всеки нов екран се компилира при първото отваряне, причинявайки закъснение от 100–500 ms. Подгответе Baseline Profile за ключовите екрани и включете генерирането в Gradle чрез baseline-profile-gradle-plugin.
Hot path — код, който се изпълнява при всеки кадър: onBindViewHolder, draw, layoutSubviews. Избягвайте създаването на обекти в тези методи: използвайте пул от обекти, StringBuilder вместо конкатенация, кеширайте форматирани низове и форматиращи устройства. Всяка допълнителна алокация приближава следващия GC.
Добавете в CI pipeline стартиране на Macrobenchmark със сценарий за превъртане на списък и отваряне на екран. Задайте праг: 99-ият персентил на времето за кадър не трябва да надвишава 16 ms. Ако прагът е надвишен — сборката се отхвърля до оптимизация.
Често задавани въпроси
Лаг — постоянно закъснение (напр. 200 ms на всяко натискане). Забавя — непостоянно: приложението работи нормално, след което внезапно се забавя за 1–3 секунди, след което отново нормално. Причина — събитийни фактори като GC паузи или синхронни заявки към базата данни.
Използвайте Memory Profiler в Android Studio: раздел Memory показва GC събития с продължителност. За производствен мониторинг свържете Firebase Performance Monitoring с персонализирани трасета. На iOS включете Malloc Debug и маркирайте поколенията на алокациите в Instruments.
Косвено — да. Ако отговорът на сървъра идва със закъснение, а UI го очаква синхронно, приложението замръзва. Ако заявката е асинхронна, но обработката на отговора се извършва в UI нишка — това също ще причини забавяне. Решение — асинхронна обработка с корутини и индикатори за напредък.
При неправилна употреба KMP може да генерира излишни обекти-обвивки за интероперабилност. На iOS това увеличава честотата на алокациите и, като следствие, ARC паузите. Използвайте @ObjCName, оптимизирайте expect/actual и избягвайте чести извиквания на споделен код от горещи пътища на UI.
Увеличаването на купчината чрез android:largeHeap="true" отлага GC, но не премахва причината за алокациите. Когато GC все пак се стартира, паузата ще бъде по-дълга, защото трябва да се обходят повече обекти. Решение — намалете броя на алокациите, не разширявайте купчината.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също