Тестування потребування оперативної пам'яті мобільним додатком
Додаток не падає одразу — він поступово зростає у пам'яті. Через 20 хвилин використання з'являється легка задумчивість. Через 40 — системний Memory Pressure вбиває фонові процеси. Через годину — SIGKILL від iOS або OOM-киллер Android завершує додаток. Користувач думає, що додаток «глючить». Firebase Crashlytics ничего не покаже — це не крєш, це убийство системою.
iOS: Instruments Allocations та Leaks
Два шаблони для аналізу пам'яті у 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 у довгоживущих об'єктах. Instruments Leaks це знайде, але часто показує симптом, а не причину. Слідуємо по графу у стек, ищемо кореневий strong reference.
UIImage та пам'ять
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 у статичних полях або синглтонах:
// Погано
object ImageCache {
var context: Context? = null // утримує Activity
}
// Правильно: applicationContext
object ImageCache {
lateinit var appContext: Context
fun init(context: Context) {
appContext = context.applicationContext // не Activity
}
}
Незакритий Cursor від ContentProvider або SQLiteDatabase:
val cursor = db.query(...)
try {
// робота з курсором
} finally {
cursor.close() // обов'язково, навіть при винятку
}
Listener, не знятий при onDestroy:
override fun onStart() {
super.onStart()
locationManager.requestLocationUpdates(provider, 0, 0f, this)
}
override fun onStop() {
super.onStop()
locationManager.removeUpdates(this) // інакше Activity не вмре
}
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.
Що включено
- Профілювання пам'яті через Instruments Allocations / Memory Profiler / DevTools
- Налаштування LeakCanary для Android-проекту
- Аналіз heap-dumps та пошук retain-циклів
- Аудит роботи з зображеннями (кеш, декодирування)
- Перевірка паттернів роботи з listeners, subscriptions, closures
- Звіт з конкретними утечками та правками
Строки
2–3 дні в залежності від розміру додатку та кількості знайдених проблем. Вартість розраховується індивідуально.







