Витік пам'яті та роздуття — що це, причини та як уникнути

Автор: IT Sectr Опубліковано: 2026-07-28 Час читання: 10 хв

Витік пам'яті — одна з найпідступніших проблем у мобільній розробці. Пам'ять застосунку невпинно зростає, поки не досягає межі, встановленої ОС, після чого слідує OutOfMemoryError або примусове завершення. За даними Square Engineering, близько 40% Android-застосунків мають хоча б один витік пам'яті, який можна виявити лише при профілюванні. Розглянемо причини та методи запобігання зростанню пам'яті.

Головне

  • GC reachability — об'єкт не видаляється, якщо на нього є активне посилання з кореневого набору
  • Статичні посилання на Activity або Context — найчастіша причина витоку в Android
  • LeakCanary — стандартний інструмент для автоматичного виявлення витоків у Android
  • WeakReference — рішення для посилань, які не повинні перешкоджати збиранню сміття
  • Lifecycle-aware компоненти автоматично скасовують підписки при знищенні в'ю

Що таке витік пам'яті та роздуття застосунку?

Витік пам'яті — ситуація, коли об'єкт, який більше не потрібен застосунку, продовжує утримуватися в heap, тому що на нього залишається активне посилання від кореневого набору (GC Root). Збирач сміття вважає такий об'єкт живим і не видаляє його.

Роздуття пам'яті — ширша проблема, коли застосунок споживає більше пам'яті, ніж необхідно для виконання поточних завдань. Причини: надмірне кешування, дублювання об'єктів, неоптимальні структури даних та фрагментація heap.

У Android на кожен застосунок виділяється обмежений heap (зазвичай 64–512 MB залежно від пристрою та версії ОС). У iOS обмеження менш жорстке, але система надсилає memory warning при наближенні до ліміту.

ХарактеристикаAndroidiOS
Обмеження heap64–512 MB (залежить від пристрою)Неявне (системне)
Збирання сміттяART (Concurrent, Compact)ARC (Automatic Reference Counting)
Механізм витокуGC Root referencesRetain cycles (цикли сильних посилань)
РезультатOutOfMemoryErrorMemory warning → завершення

За даними Facebook Engineering Blog, витоки пам'яті є причиною ~15% crash-репортів у мобільних застосунках. В Android до цього додаються ANR через часті GC паузи при нестачі пам'яті.

Типові патерни витоків пам'яті в Android та iOS

Статичне посилання на Activity — класика Android-витоків. Якщо статичне поле або синглтон зберігає посилання на Activity, вона не буде зібрана GC навіть після finish(), поки синглтон живий. Activity — важкий об'єкт, що містить View hierarchy, ресурси та Context.

kotlin
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 піти на збирання сміття.

  • Handler із затримкою — якщо Activity знищена, а Handler.postDelayed ще не виконаний, Activity витікає
  • Thread та AsyncTask — при повороті екрана Activity перестворюється, а старий Thread продовжує тримати посилання на стару Activity
  • Retrofit/Callback — анонімний Callback утримує посилання на презентер або фрагмент
  • Спостерігачі — підписки LiveData або RxJava без відписки при onDestroy

У iOS основна проблема — retain cycles: два об'єкти тримають сильні посилання один на одного, і ARC не може обнулити лічильник посилань для жодного з них. Типовий випадок: closure, що захоплює self сильно, і self, що тримає посилання на closure.

Як виявити витоки пам'яті?

LeakCanary — бібліотека від Square для автоматичного виявлення витоків в Android. Після знищення Activity або Fragment вона перевіряє, чи об'єкт був зібраний GC. Якщо ні — робить heap dump і показує trace витоку.

kotlin
// 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, що виключає основний клас витоків.

kotlin
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-розробці.

  • Не використовуйте статичні посилання на Context, Activity, View або Fragment
  • Скасовуйте всі RxJava підписки в disposeBag / CompositeDisposable при onDestroy
  • Використовуйте [weak self] / [unowned self] в iOS-замиканнях для запобігання retain cycles
  • Перевіряйте Bitmap та великі об'єкти — вони повинні бути recycled або nullified

Інструменти для профілювання пам'яті

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 червоним кольором.

ІнструментПлатформаОсобливість
LeakCanaryAndroidАвтовиявлення витоків після destroy
Memory ProfilerAndroid StudioHeap dump + live allocations
Eclipse MATAndroidDominator tree, Leak Suspects Report
Memory GraphiOS (Xcode)Візуалізатор retain cycles

За даними Google I/O 2023, застосунки, що використовують LeakCanary в debug-збірках, скорочують кількість memory-related crash на 30–50% за перші 2 місяці після впровадження. Рекомендується додавати LeakCanary на етапі onboarding проекту.

Часті запитання

Чим відрізняється витік пам'яті від роздуття?

Витік — об'єкти, не доступні коду, але не видалені GC через активні посилання. Роздуття — застосунок тримає в пам'яті об'єкти, які логічно потрібні, але в надмірній кількості (наприклад, кеш на 50 MB при працюючому застосунку вагою 80 MB). Роздуття лікується архітектурно, витік — через коректне управління посиланнями.

Як LeakCanary знаходить витоки?

LeakCanary використовує ObjectWatcher — після onDestroy() Activity він створює WeakReference на Activity та запускає GC. Якщо через 5 секунд WeakReference не очищений, LeakCanary робить heap dump, аналізує найкоротший reference chain від GC Root до об'єкта та показує точний стек витоку із зазначенням файлу та рядка коду.

Чому Bitmap часто викликає OutOfMemoryError?

Bitmap займає пам'ять поза heap Java в нативній пам'яті (native heap). Розмір одного Bitmap = ширина × висота × 4 байти (ARGB_8888). Фото 12 MP (4000×3000) займає 48 MB. Android не завжди може своєчасно звільнити native memory, що при накопиченні кількох Bitmap призводить до OOM навіть при достатньому Java heap.

Що таке retain cycle в iOS?

Retain cycle — ситуація в ARC, коли два об'єкти тримають сильні посилання один на одного, і лічильник посилань ніколи не досягає нуля. Типовий приклад: ViewController з сильним посиланням на closure, а closure захоплює self сильно. Рішення: використовувати [weak self] або [unowned self] в замиканнях.

Який максимальний розмір heap на Android?

Розмір heap залежить від пристрою та версії Android. Для старих пристроїв (API 15–24) — 64–128 MB. Для сучасних (API 25+) — 256–512 MB. Точне значення можна отримати через ActivityManager.getMemoryClass(). Для великих застосунків (ігри, редактори) є largeHeap=true в маніфесті, що дає до 1 GB.

Підсумки

  • Витік пам'яті — об'єкт, не видалений GC через активне посилання від кореневого набору; роздуття — надмірне споживання пам'яті без явних витоків
  • Статичні посилання на Activity, Context, View — номер один серед причин витоків в Android; рішення — WeakReference або Application Context
  • Анонімні класи та лямбди неявно тримають посилання на зовнішній клас; нескасовані колбеки — друга за частотою причина
  • LeakCanary — стандарт автовиявлення витоків в Android; інтеграція займає 5 хвилин і скорочує crash rate на 30–50%
  • lifecycleScope та viewModelScope автоматично скасовують корутини при destroy, виключаючи цілий клас витоків
  • Retain cycles в iOS вирішуються через weak/unowned self в замиканнях та делегатах
  • Профілюйте пам'ять не рідше одного разу на спринт — heap dump з MAT або Memory Graph має стати частиною code review

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також