Виявлення та виправлення утечок пам'яті в мобільних програмах
Програма вбиває iOS, коли користувач відкриває та закриває екран карти 20 разів підряд. Crash log містить Terminated due to memory pressure. В Instruments → Allocations видно: кожне відкриття MapViewController додає ~12 MB до heap і ці 12 MB ніколи не звільняються. Після 20 відкриттів — 240 MB тільки від екрана карти. Це утечка пам'яті — не «можливо», а точно.
Механіка утечок: чому об'єкти не звільняються
Retain cycles на iOS (ARC)
ARC лічить сильні посилання. Якщо A тримає B, а B тримає A — жоден не досягне нульового лічильника та ніколи не звільниться. Найчастіші паттерни:
Closure без [weak self]:
// УТЕЧКА
viewModel.onDataLoaded = {
self.tableView.reloadData() // сильне посилання на self
}
// ПРАВИЛЬНО
viewModel.onDataLoaded = { [weak self] in
self?.tableView.reloadData()
}
Timer:
// УТЕЧКА — Timer тримає target сильно
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true)
При закритті ViewController timer продовжує працювати та тримає ViewController. Рішення — Timer.scheduledTimer(withTimeInterval:repeats:block:) з [weak self] в block та обов'язковий timer.invalidate() у deinit.
Delegate без weak:
// УТЕЧКА
protocol DataDelegate: AnyObject { func didLoad() }
class DataService {
var delegate: DataDelegate? // повинен бути weak!
}
Якщо DataService живе довше делегата або обидва тримають один одного — утечка. weak var delegate: DataDelegate? — обов'язково.
Утечки на Android
Context leak — найпоширеніша:
// УТЕЧКА — Activity Context у singleton
object AppRepository {
private var context: Context? = null
fun init(ctx: Context) { context = ctx } // ctx — Activity
}
Activity не звільняється поки живе Repository. Використовуйте лише applicationContext у singleton-об'єктах.
Anonymous inner class + Handler:
// УТЕЧКА — анонімний клас неявно тримає посилання на Activity
private val handler = Handler(Looper.getMainLooper())
handler.postDelayed({
updateUI() // this — неявне посилання на Activity
}, 5000)
Рішення: WeakReference<MyActivity> або lifecycleScope.launch { delay(5000); updateUI() } — coroutine отменяется разом з lifecycle.
LiveData observers без removeObserver:
У Fragment підписуємося на ViewModel.liveData.observe(this, ...). Якщо this — не viewLifecycleOwner а сам Fragment — observer живе весь lifecycle Fragment, включаючи періоди коли View розрушена. При пересоздании View — додається другий observer. Після N пересозданий N observers.
Діагностика: інструменти
LeakCanary (Android) — де факто стандарт. Додайте одну залежність у debug build, він автоматично відслідковує Activity, Fragment, View, ViewModel. При виявленні утечки — сповіщення з повною retain tree. Обов'язково в будь-якому Android-проекті.
Instruments → Leaks (iOS) — будує граф об'єктів та шукає цикли. Запустіть сценарій 10–15 разів, чекайте поки Leaks почервенить. Клік на утечку — повний стек об'єктів з retain-шляхом.
Instruments → Allocations, Generation Analysis — для logical leaks (об'єкти без циклічних посилань, але які накопичуються). Mark generation перед дією → виконайте дію → подивіться що додалось та не пішло.
Android Studio Memory Profiler → Heap Dump — знімок heap з шляхом до GC root для кожного об'єкта. Шукаємо Activity екземпляри — не повинно бути більше одного (активного).
Випадок: RxJava Disposable без dispose
Flutter-розробник перейшов на Android та написав RxJava-код з Observable.interval. Підписка створювалася в onCreate, Disposable нікуди не зберігався. При кожному повороту екрану створювався новий Observer, старий продовжував працювати. Після 10 поворотів — 10 активних потоків. LeakCanary знайшов це за 2 хвилини: retained Activity через Observable → Observer → Activity reference.
Рішення: CompositeDisposable, додайте всі підписки, викличте disposables.clear() у onStop() або onDestroy().
Що ми робимо в рамках послуги
- Додаємо LeakCanary у debug-сборку Android, налаштовуємо прогон тестових сценаріїв
- Проводимо сесії Instruments Leaks + Allocations для iOS по всім ключовим екранам
- Аналізуємо всі retain-шляхи знайдених утечок
- Виправляємо: weak references, timer invalidation, proper closure capture, observer lifecycle
- Додаємо
deinit/onDestroyлогування для регресійного контролю
Часові рамки
Діагностика утечок пам'яті — 1–3 дні. Виправлення виявлених утечок — 2–7 днів залежно від кількості та запущеності проблеми.







