Цурење меморије у мобилним апликацијама — шта је то, узроци и методе откривања

Аутор: 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. Retrofit callback који затвара 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 може радити као део тест пајплајна: покрените 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 понекад даје false positive: објекат може бити привремено задржан од стране система (нпр. 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. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође