Додаток не крешиться одразу — він поступово зростає в пам'яті. Через 20 хвилин використання з'являється легка задумливість. Через 40 — системний Memory Pressure вбиває фонові процеси. Через годину — SIGKILL від iOS або OOM-кілер Android завершує додаток. Користувач думає, що додаток «глючить». Firebase Crashlytics нічого не покаже — це не крэш, це вбивство системою. Навіть якщо Memory Warning спрацьовує, UI може стати гальмівним через скидання кешів, що веде до негативних відгуків. За статистикою, 80% мобільних додатків мають витоки пам'яті, які призводять до втрати 15% користувачів на місяць. Ми, як мобільні розробники з 10-річним досвідом, щодня стикаємося з такими кейсами. Наші інженери знаходять витоки там, де стандартні інструменти мовчать. Економія на серверних витратах може сягати 30% при усуненні витоків. Зв'яжіться з нами — отримайте консультацію з профілювання вашого додатку.
Як провести профілювання пам'яті за 5 кроків
- Налаштування інструментів: Підключіть Instruments для iOS, LeakCanary для Android, DevTools Memory для Flutter.
- Сценарій тестування: Визначте ключові екрани та дії (навігація, завантаження даних, робота з зображеннями).
- Збір даних: Запустіть профілювальник, виконуйте сценарій, фіксуйте snapshots heap до та після кожної дії.
- Аналіз: Шукайте об'єкти, які не звільняються (retain cycles, listeners, контекст). Використовуйте LeakCanary для автоматичного детекту.
- Оптимізація: За знайденими витоками вносьте правки (weak references, cancel subscriptions, close cursors). Повторіть тест.
Детальніше про налаштування LeakCanary
Підключіть через debugImplementation: `debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")`. Після запуску при витоку з'явиться notification з повним стеком.Як знайти витік пам'яті на iOS?
Два шаблони для аналізу пам'яті в Instruments:
Allocations — всі виділення пам'яті, живі та мертві об'єкти. Генерації (кнопка Generation) дозволяють порівняти об'єкти в пам'яті до та після дії. Якщо після закриття екрану об'єкти цього екрану залишилися в живій пам'яті — витік.
Leaks — автоматичний детектор retain-циклів. Червона іконка = знайдений цикл. Показує граф залежностей з винуватцями.
Класичний retain-цикл в Swift:
// Витік: ViewController тримає closure, closure захоплює ViewController class PhotoViewController: UIViewController { var onPhotoLoaded: (() -> Void)? override func viewDidLoad() { super.viewDidLoad() onPhotoLoaded = { self.imageView.image = UIImage(named: "photo") // strong capture } } } // Правильно: onPhotoLoaded = { [weak self] in self?.imageView.image = UIImage(named: "photo") } [weak self] — стандарт для будь-яких closure, що захоплюють self в довгоживучих об'єктах. Інструмент Leaks це знайде, але часто показує симптом, а не причину. Слідуємо по графу в стек, шукаємо кореневий strong reference.
Згідно з Apple Instruments Documentation, використання поколінь дозволяє виявити до 90% витоків.
Чому зображення з'їдають пам'ять?
UIImage(named:) кешує зображення в системному кеші. Для часто використовуваних іконок — добре. Для великих фото, які завантажуються один раз — ні. Використовуємо UIImage(contentsOfFile:) — не кешує.
Декодування зображення відбувається при першому відображенні, не при створенні UIImage. Попереднє декодування на background thread:
func decodedImage(_ image: UIImage) -> UIImage { UIGraphicsBeginImageContextWithOptions(image.size, true, 0) defer { UIGraphicsEndImageContext() } image.draw(in: CGRect(origin: .zero, size: image.size)) return UIGraphicsGetImageFromCurrentImageContext() ?? image } Після цього виклику зображення вже декодовано і лежить в пам'яті як bitmap. Передаємо в UI без затримки на декодування.
Android: Memory Profiler та LeakCanary
Android Studio Memory Profiler показує Heap в реальному часі: Java Heap, Native Heap, Stack, Code, Graphics. Кнопка Dump Heap зберігає знімок — аналізуємо в hprof viewer або конвертуємо для Eclipse Memory Analyzer.
Але найкорисніший інструмент в бою — LeakCanary. Підключається в debugImplementation, працює автоматично:
// build.gradle.kts debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14") При виявленні витоку LeakCanary показує notification з повним стеком: що утримує що, через який ланцюжок. Не потрібно вручну аналізувати heap-dump.
Часті причини витоків на Android:
- Context в статичних полях або синглтонах.
- Незакритий Cursor від ContentProvider або SQLiteDatabase.
- Listener, не знятий при onDestroy.
Порівняння інструментів профілювання:
| Платформа | Інструмент | Метод виявлення | Складність |
|---|---|---|---|
| iOS | Instruments Allocations | Знімки heap з генераціями | Середня |
| iOS | Instruments Leaks | Автопошук retain-циклів | Низька |
| Android | Memory Profiler | Dump heap + MAT | Висока |
| Android | LeakCanary | Автоматичний моніторинг | Низька |
| Flutter | DevTools Memory | Snapshot груп об'єктів | Середня |
LeakCanary знаходить витоки в 10 разів швидше ручного аналізу heap-dump. На практиці 80% витоків відбуваються через retain-цикли або неправильне управління контекстом.
Типові витоки та їх причини:
| Тип витоку | Платформа | Причина | Рішення |
|---|---|---|---|
| Retain-цикл в closure | iOS | Сильне захоплення self | Використовувати weak self |
| Context в статичному полі | Android | Activity передана в синглтон | Використовувати Application context |
| Незакритий StreamSubscription | Flutter | Відсутність cancel в dispose | Викликати cancel в dispose |
Що робити з Cursor та підписками?
override fun onStart() { super.onStart() locationManager.requestLocationUpdates(provider, 0, 0f, this) } override fun onStop() { super.onStop() locationManager.removeUpdates(this) // інакше Activity не помре } Завжди закривайте Cursor в блоці finally. Підписки на location або sensor знімайте в onStop()/onPause().
Flutter: Observatory та DevTools Memory
Flutter DevTools → Memory tab — snapshot-профілювальник. Показує групи об'єктів за типом. Dart:core, package:myapp — дивимося на класи з несподівано великою кількістю екземплярів.
Типовий витік в Flutter — StreamSubscription без cancel():
class MyWidget extends StatefulWidget { ... } class _MyWidgetState extends State<MyWidget> { late StreamSubscription _sub; @override void initState() { super.initState(); _sub = someStream.listen((event) { ... }); } @override void dispose() { _sub.cancel(); // обов'язково super.dispose(); } } Без _sub.cancel() в dispose() підписка живе довше віджета, утримує замикання з посиланням на State.
Наш підхід
Ми проводимо повне профілювання з використанням Apple Instruments documentation та LeakCanary official site. Наші інженери сертифіковані Apple та Google, мають досвід понад 10 років та ліцензії на розробку. Після аудиту ви отримаєте звіт з конкретними витоками та готовими правками коду. Вартість послуги розраховується індивідуально, але економія від усунення витоків часто окупає її за місяць.
Що входить в послугу:
- Профілювання пам'яті через Instruments Allocations / Memory Profiler / DevTools
- Налаштування LeakCanary для Android-проекту
- Аналіз heap-dumps та пошук retain-циклів
- Аудит роботи з зображеннями (кеш, декодування)
- Перевірка паттернів роботи з listeners, subscriptions, closures
- Звіт з конкретними витоками та правками
Терміни: від 2 до 3 днів залежно від розміру додатку. Замовте консультацію — ми знайдемо витоки та запропонуємо готові правки. Зв'яжіться з нами, щоб отримати оцінку вашого проекту.







