Додаток не крешиться одразу — він поступово зростає в пам'яті. Через 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 днів залежно від розміру додатку. Замовте консультацію — ми знайдемо витоки та запропонуємо готові правки. Зв'яжіться з нами, щоб отримати оцінку вашого проекту.







