Продуктивність у мобільній розробці: що це таке, які метрики та як покращувати

Автор: IT Sectr Опубліковано: 2026-03-25 Час читання: 12 хв

Повільний застосунок — головна причина, через яку користувачі видаляють програми. Частки секунди затримки при запуску або прокручуванні списку знижують утримання на десятки відсотків. Продуктивність (performance) — це не лише швидкість, а й стабільність: відсутність ANR, крашів та витоків пам'яті. У цій статті розберемо всі аспекти продуктивності: від управління пам'яттю (GC, ARC) до профілювання інструментами. Детальніше — в офіційному посібнику Android Performance.

Головне

  • ANR та Crash — головні вороги користувацького досвіду; запобігаються фоновими потоками
  • Витік пам'яті та Retain Cycle призводять до OOM-крашів; вирішуються weak-посиланнями та утилітами
  • GC (Android) та ARC (iOS) — моделі управління пам'яттю; розуміння їх роботи критичне
  • Профілювання (Instruments, Android Profiler, LeakCanary) — обов'язковий етап розробки
  • Cold Start — найважливіша метрика запуску; оптимізація Application.onCreate та лінива ініціалізація
  • Розмір застосунку — використовувати App Bundle, R8, VectorDrawable та WebP для зменшення розміру

Чому застосунок гальмує?

Продуктивність застосунку безпосередньо пов'язана з гальмуванням (jank) — помітною затримкою між дією користувача та реакцією інтерфейсу. Основні причини: блокування Main Thread (важкі операції на UI-потоці), часті перемальовування layout (overdraw), витоки пам'яті (частий GC), неоптимальні алгоритми (O(n²) на великих даних). Frame Rate (FPS) — кількість кадрів на секунду. Для комфортного досвіду потрібні стабільні 60 FPS (Android) або 120 FPS (iPhone Pro, iPad Pro). VSync — синхронізація відтворення з частотою оновлення екрана.

Jank виникає, коли відтворення одного кадру перевищує 16.6 мс (для 60 FPS) або 8.3 мс (для 120 FPS). Профілювання GPU (Profile GPU Rendering на Android, Core Animation на iOS) показує, які етапи рендерингу займають найбільше часу. Основні етапи: Layout (розстановка елементів), Draw (малювання), Display (передача в буфер кадру). Найчастіша проблема — layout inflation в XML, особливо при використанні складних вкладених ConstraintLayout.

Time-to-Interactive (TTI) — час, за який застосунок стає повністю готовим до взаємодії. TTI включає Cold Start, завантаження даних, ініціалізацію бібліотек. Google рекомендує TTI менше 5 секунд, Apple — менше 2 секунд для основних екранів. Lazy Loading — техніка відкладеного завантаження контенту та бібліотек, критична для покращення TTI. В IT Sectr ми застосовуємо ліниву ініціалізацію за замовчуванням на всіх проектах.

ANR та Crash

ANR та Crash — головні вороги продуктивності мобільного застосунку. ANR (Application Not Responding) — діалогове вікно на Android, що з'являється, якщо головний потік заблоковано більше 5 секунд. Причини: синхронні мережеві запити на UI-потоці, робота з базою даних без корутин, large bitmap decode без downsampling, deadlock на Main Thread. Стек викликів ANR зберігається в /data/anr/traces.txt і дозволяє визначити точне місце блокування.

Crash — несподіване завершення застосунку. На Android — це Exception (Java/Kotlin) або Signal (native code). На iOS — NSException або сигнал (EXC_BAD_ACCESS — звернення до звільненої пам'яті). Інструменти Crash Reporting: Firebase Crashlytics, Sentry, BugSnag. Вони збирають stacktrace, дані про пристрій та кроки відтворення. Stack Overflow — переповнення стеку викликів при нескінченній рекурсії. OutOfMemoryError — коли купа (heap) переповнена.

StrictMode — інструмент Android для виявлення порушень потокової безпеки. Дозволяє задати правила: ThreadPolicy (заборонити диск/мережу на main thread), VmPolicy (виявити витоки Activity, SQLite, CloseGuard). StrictMode рекомендується вмикати тільки в debug-складанні — в релізі він не повинен працювати. На iOS аналог — Main Thread Checker (Xcode), який автоматично виявляє UIKit-виклики не на головному потоці.

Управління пам'яттю (GC, ARC, Retain Cycle)

Витік пам'яті

Витік пам'яті (Memory Leak) — ситуація, коли об'єкт залишається в пам'яті, хоча застосунок його більше не використовує. Це безпосередньо знижує продуктивність застосунку. На Android GC (Garbage Collection) не може зібрати об'єкт, якщо на нього є сильне посилання. Типові причини: статичні посилання на Activity, нескасовані колбеки/спостерігачі, внутрішні класи з неявним посиланням на зовнішній клас, Handler з неочищеними повідомленнями. LeakCanary — бібліотека для автоматичного виявлення витоків.

Retain Cycle (Циклічне посилання)

ARC (Automatic Reference Counting) — модель управління пам'яттю в iOS. Кожен об'єкт має лічильник посилань (retain count). При обнуленні лічильника пам'ять звільняється. Retain Cycle — коли два об'єкти тримають сильні посилання один на одного (A → B і B → A). ARC ніколи не обнулить лічильники. Рішення: слабкі посилання (weak) або безхазяйні (unowned). Weak автоматично обнуляється (sets to nil) при звільненні об'єкта. Unowned — не обнуляється, але гарантує, що об'єкт живий.

GC vs ARC

GC (Garbage Collection) працює на Android (Java/Kotlin). GC періодично призупиняє виконання (Stop-the-World pause) для пошуку та звільнення недосяжних об'єктів. GC Trigger: при заповненні купи (heap) на певний відсоток. ARC працює на iOS (Swift/Objective-C) і не має пауз — лічильники оновлюються атомарно при кожному присвоєнні. ARC більш передбачуваний, але може накопичувати надлишкові retain/release при високій частоті присвоєнь.

Weak Reference та Strong Reference — тип посилання визначає, чи може GC/ARC звільнити об'єкт. Strong Reference — об'єкт не буде зібрано, поки існує це посилання. Weak Reference — GC/ARC може зібрати об'єкт; weak-посилання стане nil (у Swift/Java WeakReference). Unowned Reference (Swift) — не обнуляється при звільненні, доступ до нього після смерті об'єкта викликає краш. На Android для слабких посилань використовується java.lang.ref.WeakReference.

Приклад виявлення витоку на Android через LeakCanary:

kotlin
// Утечка: анонимный класс держит ссылку на Activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val handler = object : Handler(Looper.getMainLooper()) {
            override fun handleMessage(msg: Message) {
                // Используем `this@MainActivity`, сохраняя ссылку на Activity
                Log.d("TAG", "Handler received message")
            }
        }
        handler.sendEmptyMessageDelayed(0, 60000)
    }
}

// Исправление: статический Handler + WeakReference
class SafeHandler(activity: MainActivity) : Handler() {
    private val weakActivity =
        WeakReference(activity)

    override fun handleMessage(msg: Message) {
        weakActivity.get() ?: return
        Log.d("TAG", "Handler received message")
    }
}

Профілювання (Instruments, Android Profiler)

Профілювання — процес вимірювання продуктивності застосунку: CPU, пам'ять, мережа, енергоспоживання. Без профілювання оптимізація наосліп марна — ви не дізнаєтеся, яка частина коду реально гальмує.

Інструмент Платформа Вимірює Коли використовувати
Instruments (Time Profiler)iOSCPU, виклики функцій, час виконанняОптимізація алгоритмів, пошук вузьких місць
Instruments (Allocations)iOSПам'ять, кількість об'єктів, retain countsПошук витоків та надмірного споживання пам'яті
Instruments (Leaks)iOSRetain cycles, витоки пам'ятіРегулярна перевірка перед релізом
Android Profiler (CPU)AndroidCPU usage, thread activity, tracesПошук блокувань Main Thread
Android Profiler (Memory)AndroidHeap dump, allocation trackingПошук витоків, аналіз об'єктів
Android Profiler (Network)AndroidТрафік, швидкість, таймінги запитівОптимізація мережевих викликів
LeakCanaryAndroidАвтоматичне виявлення витоків пам'ятіНа всіх етапах розробки
StrictModeAndroidДиск/мережа на main thread, витокиDebug-складання
Traceview / SystraceAndroidМетод-трасування, системні подіїГлибокий аналіз затримок

Instruments (Xcode) — найпотужніший інструмент для iOS. Time Profiler показує, які функції споживають найбільше CPU. Allocations відстежує створення та звільнення об'єктів. Leaks автоматично знаходить retain cycles. Кроки профілювання: (1) запустити Instruments; (2) вибрати шаблон (Time Profiler для CPU); (3) виконати проблемний сценарій; (4) проаналізувати стек викликів — найширший стовпець — найгарячіша функція.

Android Profiler — вбудований в Android Studio (View → Tool Windows → Profiler). CPU Profiler показує завантаження кожного потоку. Memory Profiler — heap dump та allocation tracking. Network Profiler — всі HTTP-запити з таймінгами. Energy Profiler — споживання енергії: WakeLock, Location, Network. Для детального трасування використовується Systrace (Android 10+) або Perfetto — системний трейсинг з точністю до мікросекунд.

Запуск застосунку (Cold/Warm/Hot Start)

Запуск застосунку — один з ключових показників продуктивності. Він ділиться на три типи: Cold Start — застосунок запускається з нуля: процес створюється, Application.onCreate (Android) / AppDelegate.applicationDidFinishLaunching (iOS), завантаження класів, ініціалізація бібліотек. Warm Start — процес існує, але Activity/ViewController знищений (наприклад, при повороті екрану або поверненні з пам'яті). Hot Start — Activity/ViewController в пам'яті, застосунок просто показується (перемикання з іншого застосунку).

Cold Start — найважливіша метрика. На Android включає: (1) launch Activity — завантаження XML, ініціалізація View; (2) перший frame — час до першого відтворення. Google рекомендує: launch Activity < 200 мс, перший frame < 500 мс, TTI < 5 секунд. Оптимізація Cold Start: зменшити Application.onCreate (корутини для лінивої ініціалізації), використовувати SplashScreen API (Android 12+), відкласти ініціалізацію бібліотек (WorkManager, DI), видалити зайві ContentProviders.

На iOS Cold Start включає: завантаження Mach-O бінарного файлу, dyld (динамічний лінковщик), ініціалізація Objective-C runtime, Application delegate, перший controller. Chrome Custom Tabs (Android) та Universal Links (iOS) — технології для швидкого відкриття зовнішнього контенту в застосунку без повного Cold Start. Рекомендується тестувати Cold Start на реальних пристроях середнього сегменту.

Оптимізація розміру

Розмір застосунку — фактор продуктивності встановлення та оновлень. Він впливає на конверсію: кожні 10 MB знижують конверсію на 1%. Google Play рекомендує розмір APK менше 150 MB; App Store — менше 200 MB (стільникові мережі — 100 MB). Основні методи оптимізації: стиснення зображень (WebP замість PNG дає 25-35% економії), векторизація (VectorDrawable в Android, SF Symbols в iOS), видалення невикористовуваного коду (R8/ProGuard), видалення невикористовуваних ресурсів (lint → unused resources).

App Bundle (Android) — формат публікації, при якому Google Play генерує оптимізований APK під кожен пристрій. App Bundle зменшує розмір завантаження на 20-40%. Dynamic Delivery — модулі, які завантажуються на вимогу (on-demand feature modules). На iOS еквівалент — On-Demand Resources (ODR): ресурси, що завантажуються після першого запуску (рівні гри, відео).

Lazy Loading — техніка, при якій модулі та бібліотеки не завантажуються при запуску, а підвантажуються по необхідності. Split APK (Android) та App Slicing (iOS) — розділення застосунку на архітектурні слоти: arm64-v8a, x86_64. Оптимізація розміру застосунку — постійний процес: аналізуйте склад APK (Analyze APK в Android Studio), видаляйте дублікати іконок, використовуйте SVG замість декількох щільностей PNG. В IT Sectr ми включаємо перевірку розміру збірки в CI/CD для кожного MR.

Часті запитання

Що таке ANR і як його уникнути?

ANR (Application Not Responding) — діалог, який з'являється на Android, якщо головний потік заблоковано більше 5 секунд. Щоб уникнути ANR, виносьте всі важкі операції (мережа, база даних, обробка файлів) у фонові потоки. На iOS аналог — frozen UI, коли застосунок перестає реагувати на дотики.

Що таке Memory Leak і Retain Cycle?

Memory Leak — витік пам'яті, коли об'єкт не може бути звільнений, тому що на нього залишаються посилання. Retain Cycle — ситуація в iOS/Objective-C, коли два об'єкти посилаються один на одного (A → B → A), і ARC не може звільнити жоден. Рішення: weak/unowned посилання та своєчасне очищення колбеків.

Які інструменти використовувати для профілювання?

Для iOS: Instruments (Time Profiler, Allocations, Leaks). Для Android: Android Profiler (CPU, Memory, Network), LeakCanary (витоки пам'яті), StrictMode (порушення потоків). Рекомендується комбінувати профілювання на етапі розробки та інтеграції.

Чим відрізняється Cold Start від Warm Start та Hot Start?

Cold Start — застосунок запускається з нуля: процес створюється, класи завантажуються, Application.onCreate виконується. Warm Start — процес існує, але Activity/ViewController перестворюється. Hot Start — Activity/ViewController вже в пам'яті, просто показується. Cold Start найдовший (1-5 секунд) і критичний для користувацького досвіду.

Як зменшити розмір мобільного застосунку?

Основні методи: видалення невикористовуваних ресурсів та коду (використовуйте R8/ProGuard), векторизація зображень (VectorDrawable, SF Symbols), компресія PNG/WebP (Android), App Bundle замість APK, видалення зайвих бібліотек, Lazy Loading модулів. Оптимізація розміру може скоротити APK на 40-60%.

Підсумки

  • ANR та Crash — головні проблеми стабільності; вирішуються фоновими потоками та crash-репортерами
  • Memory Leak та Retain Cycle — основні причини OOM; вирішуються weak-посиланнями та LeakCanary
  • GC (паузи Stop-the-World) vs ARC (без пауз, але retain cycles) — різні моделі пам'яті
  • Профілювання — обов'язковий етап: Instruments (iOS), Android Profiler, LeakCanary, StrictMode
  • Cold Start — ключова метрика; оптимізація Application.onCreate та лінива ініціалізація
  • App Bundle та WebP/VectorDrawable — основні інструменти для зменшення розміру на 20-60%
  • Продуктивність — безперервний процес, а не разова активність; впроваджуйте метрики в CI/CD

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект