Витік пам'яті — одна з найпідступніших проблем у мобільній розробці. Пам'ять застосунку невпинно зростає, поки не досягає межі, встановленої ОС, після чого слідує OutOfMemoryError або примусове завершення. За даними Square Engineering, близько 40% Android-застосунків мають хоча б один витік пам'яті, який можна виявити лише при профілюванні. Розглянемо причини та методи запобігання зростанню пам'яті.
Головне
Витік пам'яті — ситуація, коли об'єкт, який більше не потрібен застосунку, продовжує утримуватися в heap, тому що на нього залишається активне посилання від кореневого набору (GC Root). Збирач сміття вважає такий об'єкт живим і не видаляє його.
Роздуття пам'яті — ширша проблема, коли застосунок споживає більше пам'яті, ніж необхідно для виконання поточних завдань. Причини: надмірне кешування, дублювання об'єктів, неоптимальні структури даних та фрагментація heap.
У Android на кожен застосунок виділяється обмежений heap (зазвичай 64–512 MB залежно від пристрою та версії ОС). У iOS обмеження менш жорстке, але система надсилає memory warning при наближенні до ліміту.
| Характеристика | Android | iOS |
|---|---|---|
| Обмеження heap | 64–512 MB (залежить від пристрою) | Неявне (системне) |
| Збирання сміття | ART (Concurrent, Compact) | ARC (Automatic Reference Counting) |
| Механізм витоку | GC Root references | Retain cycles (цикли сильних посилань) |
| Результат | OutOfMemoryError | Memory warning → завершення |
За даними Facebook Engineering Blog, витоки пам'яті є причиною ~15% crash-репортів у мобільних застосунках. В Android до цього додаються ANR через часті GC паузи при нестачі пам'яті.
Статичне посилання на Activity — класика Android-витоків. Якщо статичне поле або синглтон зберігає посилання на Activity, вона не буде зібрана GC навіть після finish(), поки синглтон живий. Activity — важкий об'єкт, що містить View hierarchy, ресурси та Context.
object LeakHolder {
var activityRef: Activity ?= null // leak: static reference to Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
}
}
Анонімні класи та лямбди — неявно утримують посилання на зовнішній клас. Якщо Runnable або Callback передається у зовнішній сервіс, а Activity знищується, об'єкт анонімного класу все ще висить у черзі та не дає Activity піти на збирання сміття.
У iOS основна проблема — retain cycles: два об'єкти тримають сильні посилання один на одного, і ARC не може обнулити лічильник посилань для жодного з них. Типовий випадок: closure, що захоплює self сильно, і self, що тримає посилання на closure.
LeakCanary — бібліотека від Square для автоматичного виявлення витоків в Android. Після знищення Activity або Fragment вона перевіряє, чи об'єкт був зібраний GC. Якщо ні — робить heap dump і показує trace витоку.
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary auto-installs in debug build
// via ContentProvider — zero code setup
}
}
// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — вбудований інструмент для моніторингу пам'яті в реальному часі. Дозволяє записати heap dump, знайти об'єкти-підозрювані (Retained Size > 1 MB) та прослідкувати GC root path до кожного об'єкта.
Для iOS використовуйте Xcode Memory Graph Debugger. Він візуалізує граф об'єктів у пам'яті, показує retain cycles та дозволяє миттєво виявити кругові посилання. Також доступний Instruments > Allocations для довгострокового моніторингу.
WeakReference — базовий механізм для посилань, які не повинні заважати збиранню сміття. Якщо GC вирішить видалити об'єкт, WeakReference поверне null. Використовується для зворотних викликів, слухачів та посилань на UI-компоненти з фонових потоків.
Lifecycle-aware компоненти — архітектурний підхід, реалізований в Android Jetpack (Lifecycle, LiveData, Flow, coroutines). Підписки автоматично скасовуються при onDestroy, що виключає основний клас витоків.
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
fun loadData() {
viewModelScope.launch {
val result = repository.fetchData()
_data.postValue(result)
// coroutine auto-cancels on onCleared()
}
}
}
viewModelScope та lifecycleScope — вбудовані CoroutineScope в Android, які скасовуються при відповідній події життєвого циклу. Це виключає витоки через корутини — найчастіший сценарій у сучасній Android-розробці.
Memory Profiler in Android Studio — основний інструмент для моніторингу heap. Показує live allocations, знімки heap, кількість об'єктів за типами. Дозволяє записати дамп та проаналізувати його в MAT (Memory Analyzer Tool) для пошуку підозрілих об'єктів.
Eclipse MAT — десктопний аналізатор heap dump. Після завантаження HPROF-файлу з Android Studio, MAT будує дерево dominator, показує retain size кожного об'єкта та пропонує автоматичний аналіз підозрілих витоків через Leak Suspects Report.
Xcode Memory Graph — візуальний дебагер retain cycles. При натисканні на кнопку Memory Graph Debugger Xcode зупиняє застосунок, будує повний граф об'єктів у пам'яті та підсвічує retain cycles червоним кольором.
| Інструмент | Платформа | Особливість |
|---|---|---|
| LeakCanary | Android | Автовиявлення витоків після destroy |
| Memory Profiler | Android Studio | Heap dump + live allocations |
| Eclipse MAT | Android | Dominator tree, Leak Suspects Report |
| Memory Graph | iOS (Xcode) | Візуалізатор retain cycles |
За даними Google I/O 2023, застосунки, що використовують LeakCanary в debug-збірках, скорочують кількість memory-related crash на 30–50% за перші 2 місяці після впровадження. Рекомендується додавати LeakCanary на етапі onboarding проекту.
Часті запитання
Витік — об'єкти, не доступні коду, але не видалені GC через активні посилання. Роздуття — застосунок тримає в пам'яті об'єкти, які логічно потрібні, але в надмірній кількості (наприклад, кеш на 50 MB при працюючому застосунку вагою 80 MB). Роздуття лікується архітектурно, витік — через коректне управління посиланнями.
LeakCanary використовує ObjectWatcher — після onDestroy() Activity він створює WeakReference на Activity та запускає GC. Якщо через 5 секунд WeakReference не очищений, LeakCanary робить heap dump, аналізує найкоротший reference chain від GC Root до об'єкта та показує точний стек витоку із зазначенням файлу та рядка коду.
Bitmap займає пам'ять поза heap Java в нативній пам'яті (native heap). Розмір одного Bitmap = ширина × висота × 4 байти (ARGB_8888). Фото 12 MP (4000×3000) займає 48 MB. Android не завжди може своєчасно звільнити native memory, що при накопиченні кількох Bitmap призводить до OOM навіть при достатньому Java heap.
Retain cycle — ситуація в ARC, коли два об'єкти тримають сильні посилання один на одного, і лічильник посилань ніколи не досягає нуля. Типовий приклад: ViewController з сильним посиланням на closure, а closure захоплює self сильно. Рішення: використовувати [weak self] або [unowned self] в замиканнях.
Розмір heap залежить від пристрою та версії Android. Для старих пристроїв (API 15–24) — 64–128 MB. Для сучасних (API 25+) — 256–512 MB. Точне значення можна отримати через ActivityManager.getMemoryClass(). Для великих застосунків (ігри, редактори) є largeHeap=true в маніфесті, що дає до 1 GB.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також