Затеже — то је кориснички опис ситуације када мобилна апликација ради споро и нестабилно: час реагује нормално, час изненада застане на неколико секунди. У техничком контексту „затеже” означава комбинацију лагова и микрозастајања изазвану честим 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. Избегавајте стварање објеката у овим методама: користите pool објеката, StringBuilder уместо конкатенације, кеширајте форматиране стрингове и форматере. Свака додатна алокација приближава следећи GC.
Додајте у CI pipeline покретање Macrobenchmark са сценаријом скроловања листе и отварања екрана. Поставите праг: 99. перцентил времена кадра не сме да прелази 16 ms. Ако је праг прекорачен — build се одбија до оптимизације.
Често постављана питања
Лаг — стално кашњење (нпр. 200 ms на сваки притисак). Затеже — непостојано: апликација ради нормално, затим изненада успорава 1–3 секунде, па опет нормално. Узрок — догађајни фактори попут GC пауза или синхроних упита ка бази података.
Користите Memory Profiler у Android Studio: картица Memory приказује GC догађаје са трајањем. За производни мониторинг прикључите Firebase Performance Monitoring са прилагођеним траговима. На iOS укључите Malloc Debug и означите генерације алокација у Instruments-у.
Индиректно — да. Ако одговор сервера долази са кашњењем, а UI га чека синхроно, апликација застаје. Ако је упит асинхрон, али се обрада одговора обавља у UI нити — то ће такође изазвати затезање. Решење — асинхрона обрада са corutinama и индикаторима напретка.
При неправилној употреби KMP може генерисати сувишне омотаче за интероперабилност. На iOS то повећава учесталост алокација и, последично, ARC паузе. Користите @ObjCName, оптимизујте expect/actual и избегавајте честе позиве заједничког кода из врућих путања UI.
Повећање хеапа кроз android:largeHeap="true" одлаже GC, али не отклања узрок алокација. Када GC ипак покрене, пауза ће бити дужа јер треба обићи више објеката. Решење — смањити број алокација, а не проширивати хеап.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође