Изтичане на памет в мобилни приложения — какво е, причини и методи за откриване

Автор: IT Sectr Публикувано: 2026-03-29 Време за четене: 9 мин

Изтичане на памет (Memory Leak) — ситуация, при която приложението задържа референции към обекти, които вече не са необходими, предотвратявайки събирача на отпадъци (GC) да освободи occupied memory. Според данните на LeakCanary, дори в добре написани приложения се срещат 3–5 изтичания на 10 000 реда код. Всяко изтичане постепенно намалява наличната памет, водещо до забавяния и OutOfMemoryError.

Основни точки

  • Memory Leak — обектът остава в паметта, въпреки че няма активни референции от логиката на приложението
  • Статични референции към Activity или Context — най-честата причина за изтичания в Android
  • LeakCanary — стандартният инструмент за автоматично откриване на изтичания в Android
  • WeakReference и Application Context — основни техники за предотвратяване на изтичания
  • Lifecycle-aware компоненти премахват цял клас изтичания, свързани с абонаменти

Какво е изтичане на памет

Изтичане на памет (Memory Leak) — ситуация, при която даден обект остава достижим чрез верига от силни референции (Strong Reference), въпреки че логически вече не е необходим на приложението. Събирачът на отпадъци (GC) счита такъв обект за жив и не освобождава заетата от него памет. В резултат наличната памет на купчината (Heap) непрекъснато намалява, а честотата на GC паузите се увеличава.

За разлика от езиците с ръчно управление на паметта (C, C++), в Java/Kotlin изтичането не е забравен free(), а забравена референция. Докато съществува strong reference от кореновия обект (GC Root) до изтичащия обект, GC го счита за необходим. Типични GC Root: статични полета, активни нишки, стек на извикванията, JNI-глобални референции.

Опасността от изтичания е техният кумулативен ефект. Едно изтичане от 100 KB не се забелязва, но 100 такива изтичания заемат 10 MB и приложението започва да се забавя поради чести GC. Критичната маса от изтичания води до OutOfMemoryError и срив на приложението. Симптоми на изтичане: постоянно нарастване на консумацията на памет в графиката на Profiler, чести GC паузи с STW (Stop The World) и намаляване на производителността на UI.

Типични видове изтичания в мобилни приложения

Пет вида изтичания покриват 95% от случаите в мобилното разработване. Всеки има своя причина и характерен модел в кода.

Статична референция към Activity или Context

Най-известното изтичане в Android — съхраняване на статична референция към Activity или Context. Типичен код: статично поле Activity, което не се нулира при onDestroy(). Докато статичното поле е живо, цялото Activity с неговото View дърво, което може да заема 1–10 MB, остава живо. Това е класическо изтичане, което LeakCanary намира на първо място.

Решение: никога не съхранявайте Activity или Context в статични полета. Използвайте Application Context за сингълтони, които надживяват Activity. Ако имате нужда от референция към Activity — използвайте WeakReference<Activity>.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Вътрешни класове с имплицитна референция

Анонимни класове и нестатични вложени класове имплицитно съхраняват референция към класа, който ги съдържа. Runnable, предаден на Handler, който се изпълнява след onDestroy(), задържа цялото Activity. Callback на Retrofit, който затваря Activity, прави същото. Това е най-коварният тип изтичане — имплицитната референция не се вижда в кода.

Kotlin object-изразите и ламбдите също улавят референции към външния клас. Правете вложените класове статични (или top-level в Kotlin) и предавайте външните референции чрез WeakReference. За ламбди използвайте Lifecycle-aware подход с viewLifecycleOwner.

Неотписани слушатели и абонаменти

Абонамент за системни услуги без отписване — директно изтичане. SensorManager, LocationManager, NotificationListener, регистрирани в onResume() без извикване на unregister в onPause(), задържат Activity. Аналогично: RxJava Disposable, недобавен към CompositeDisposable, и корутина, стартирана чрез GlobalScope.

Използвайте Lifecycle-aware компоненти: observe() с LifecycleOwner автоматично се отписва при onDestroy(). За RxJava — viewLifecycleOwner.lifecycle.addObserver с DisposableObserver. За корутини — lifecycleScope.launch() е обвързан с жизнения цикъл.

kotlin
// автоматично отписване чрез Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// корутини с lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap без recycle

Bitmap заема значително количество памет на купчината: един FullHD-битмап — 1920 × 1080 × 4 байта = 8.3 MB. Ако Bitmap се създава за всеки елемент от списъка и не се извиква recycle() при скриване, паметта бързо се изчерпва. На стари версии на Android (преди 3.0) Bitmap се съхраняваше в native памет, но на съвременни — в купчината Dalvik/ART, и GC може да го освободи само ако няма strong reference.

Използвайте Glide или Coil за зареждане на изображения — тези библиотеки управляват кеширането и recycle автоматично. Ако работите директно с Bitmap, извиквайте bitmap.recycle() за големи изображения, които вече не се показват, и използвайте inSampleSize за зареждане на умалени копия.

Fragment Reference след onDestroyView

Fragment има два жизнени цикъла: на самия Fragment и на неговото View. След onDestroyView() View дървото се унищожава, но самият Fragment може да остане в паметта, ако има външна референция. Типична грешка — съхраняване на референция към Fragment в адаптер на ViewPager или в навигационен граф, която не се изчиства при унищожаване.

Никога не съхранявайте референция към Fragment в полета на дългоживеещи обекти. Използвайте childFragmentManager за вложени фрагменти и observe() с LifecycleOwner за предаване на данни между тях. ViewPager2 реши този проблем на ниво API: FragmentTransactionAdapter коректно управлява жизнения цикъл.

Как да открием изтичане на памет

Откриването на изтичане изисква проверка на два факта: паметта не се връща след expected lifetime и броят на обектите от определен тип расте без намаляване. Процесът на диагностика включва три етапа.

Първи етап — визуална проверка чрез Memory Profiler в Android Studio. Отворете раздела Memory, изпълнете целевото действие (отворете и затворете екрана), натиснете GC (Garbage Collection) и проверете дали паметта се е върнала до първоначалното ниво. Ако след 3–4 цикъла на отваряне-затваряне паметта постоянно расте — има изтичане.

Втори етап — вземане на Heap Dump. В Memory Profiler натиснете Dump Java Heap. Полученият .hprof файл отворете в Android Studio: ще видите всички обекти в купчината с размери и референции. Търсете класове, чийто брой трябва да бъде нула след затваряне на екрана. Например MainActivity с брой 2 след затваряне — явно изтичане.

Трети етап — анализ на Retained Size и GC Root. В Android Studio анализирайте Retained Size: колко памет ще се освободи, ако изтриете този обект. Пътят от GC Root до обекта показва какво го задържа: Static field → HashMap → Activity — и виждате точката на изтичане. Панелът Reference widget показва всички притежатели на обекта.

Инструменти за търсене на изтичания

Четири инструмента покриват търсенето на изтичания от автоматично откриване до задълбочен анализ на Heap Dump.

ИнструментМетодФормат на резултатите
LeakCanaryАвтоматичен мониторингHeap Dump + stack trace на изтичането
Android Memory ProfilerРъчен мониторингГрафика на паметта + Heap Dump
MAT (Eclipse)Задълбочен анализДоклад Dominator Tree + път на GC Root
PerfettoSystem-wide проследяванеВремева линия + native памет

LeakCanary — must-have за всеки Android проект. Той автоматично открива изтичания след приключване на жизнения цикъл на Activity/Fragment и показва точното място на изтичането със stack trace. Интеграция: един ред в build.gradle. LeakCanary 2.x не изисква ръчна инициализация — автоматично регистрира Application Watcher.

Как да предотвратим изтичания на памет

Профилактиката на изтичания се вгражда в процеса на разработка чрез набор от правила и инструменти, които проверяват кода на всеки етап.

Правило за силни референции

Никога не съхранявайте референция към Activity, Fragment или View в статично поле, сингълтон или дългоживеещ обект. Ако референцията е неизбежна — използвайте WeakReference или съхранявайте данни чрез ViewModel, който живее точно толкова, колкото е необходимо, и не задържа View директно.

Lifecycle-aware архитектура

ViewModel и LiveData от Android Architecture Components решават проблема с жизнения цикъл на архитектурно ниво. ViewModel оцелява при завъртане на екрана и не съдържа референции към View. LiveData автоматично отписва observer-а при onDestroy(). Използвайте ги вместо ръчен абонамент за системни услуги.

Code Review с фокус върху GC Root

По време на code review обръщайте внимание на: статични полета с типове Context/View, анонимни класове, ламбди, затварящи Activity, ръчни абонаменти, RxJava disposable без composite, съхраняване на Fragment чрез Bundle. В Kotlin допълнително проверявайте корутините за launch без обвързване с жизнения цикъл.

Автоматична проверка в CI

LeakCanary може да работи като част от тестовия pipeline: стартирайте acceptance тестове с LeakCanary и маркирайте build-а като неуспешен, ако бъде открито изтичане. Това предотвратява попадането на изтичания в продукция. Допълнете проверката с Android Lint с правилото StaticFieldLeak — то намира потенциални изтичания на ниво статичен анализ.

kotlin
// LeakCanary в тестове
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // fail ако има изтичане
    }
}

Често задавани въпроси

По какво се различава изтичането на памет от OutOfMemoryError?

Изтичането е причината, а OutOfMemoryError е следствието. Едно изтичане не води до OOM, но натрупването на десетки изтичания изчерпва Heap. OOM е фатално изключение, а изтичането е модел, който води до него с течение на времето.

Как да намерим изтичане без LeakCanary?

Чрез Android Memory Profiler: отваряйте и затваряйте екрана 5 пъти, след всяко затваряне извиквайте GC. Ако паметта не се връща до базовото ниво — има изтичане. Вземете Heap Dump и намерете в списъка класа Activity, чийто брой е по-голям от 0 след затваряне.

Може ли Kotlin да предотврати изтичания на езиково ниво?

Частично. Kotlin решава проблема с null-safety, но не управлява strong references. Корутините с lifecycleScope и viewModelScope предотвратяват изтичания от фонови задачи, а sealed class и data class намаляват броя на състоянията, водещи до изтичания. Основната защита са архитектурните модели, а не функциите на езика.

Защо LeakCanary намира изтичане, което не съществува?

LeakCanary понякога дава фалшиво положителни резултати: обект може временно да бъде задържан от системата (например InputMethodManager задържа последния View). Проверете ръчно: ако Retained Size < 1 KB и GC Root е системна услуга, вероятно това е фалшива аларма.

Изтичанията на памет се случват само на Android?

Не. Изтичания са възможни на всяка платформа с GC: iOS (Swift/Objective-C), Flutter (Dart), уеб браузъри (JavaScript). Механизмите са еднакви — strong reference от GC Root. На iOS ARC автоматично управлява паметта, но retain cycle между обекти създава същото изтичане.

Резюме

  • Memory Leak — обект, който GC не може да освободи поради забравена strong reference
  • Статични референции към Activity и Context — най-честата причина за изтичания
  • Имплицитни референции чрез анонимни класове, ламбди и RxJava абонаменти са по-коварни от явните
  • LeakCanary автоматично намира изтичания и показва точен stack trace
  • Lifecycle-aware компоненти (ViewModel, LiveData, lifecycleScope) премахват цял клас изтичания
  • Heap Dump и анализ на Retained Size — основният метод за ръчна диагностика
  • Профилактиката включва code review за strong reference и CI проверка с LeakCanary

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също