Únik paměti (Memory Leak) — situace, kdy aplikace drží reference na objekty, které již nejsou potřeba, a brání garbage collectoru uvolnit occupied memory. Podle údajů LeakCanary se i v dobře napsaných aplikacích vyskytují 3–5 úniků na 10 000 řádků kódu. Každý únik postupně snižuje dostupnou paměť, což vede ke zpomalení a OutOfMemoryError.
Nejdůležitější
Únik paměti (Memory Leak) — situace, kdy objekt zůstává dosažitelný prostřednictvím řetězce silných referencí (Strong Reference), přestože logicky již není pro aplikaci potřeba. Garbage collector (GC) považuje takový objekt za živý a neuvolňuje jím zabranou paměť. V důsledku toho se dostupná paměť haldy (Heap) neustále snižuje a četnost GC pauz roste.
Na rozdíl od jazyků s ručním správou paměti (C, C++), v Java/Kotlin není únik zapomenutý free(), ale zapomenutá reference. Dokud existuje strong reference od kořenového objektu (GC Root) k unikajícímu objektu, GC jej považuje za potřebný. Typické GC Root: statická pole, aktivní vlákna, zásobník volání, JNI-globální reference.
Nebezpečí úniků je jejich kumulativní efekt. Jeden únik 100 KB není patrný, ale 100 takových úniků zabírá 10 MB a aplikace začíná zpomalovat kvůli častým GC. Kritická masa úniků vede k OutOfMemoryError a pádu aplikace. Příznaky úniku: stálý růst spotřeby paměti v grafu Profileru, časté GC pauzy s STW (Stop The World) a pokles výkonu UI.
Pět druhů úniků pokrývá 95% případů v mobilním vývoji. Každý má svou příčinu a charakteristický vzor v kódu.
Nejznámější únik v Androidu — uchovávání statické reference na Activity nebo Context. Typický kód: statické pole Activity, které se při onDestroy() nevynuluje. Dokud statické pole žije, žije celé Activity se svým View stromem, který může zabírat 1–10 MB. To je klasický únik, který LeakCanary nachází jako první.
Řešení: nikdy neuchovávejte Activity nebo Context ve statických polích. Používejte Application Context pro singly, které přežívají Activity. Pokud potřebujete referenci na Activity — použijte WeakReference<Activity>.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
Anonymní třídy a nestatické vnořené třídy implicitně uchovávají referenci na obsahující třídu. Runnable předaný Handleru, který se provádí po onDestroy(), drží celé Activity. Callback Retrofit, který uzavírá Activity, dělá totéž. To je nejzákeřnější typ úniku — implicitní reference není v kódu vidět.
Kotlin object-výrazy a lambdy také zachycují reference na vnější třídu. Dělejte vnořené třídy statické (nebo top-level v Kotlin) a předávejte vnější reference přes WeakReference. Pro lambdy používejte přístup Lifecycle-aware s viewLifecycleOwner.
Předplatné systémových služeb bez odhlášení — přímý únik. SensorManager, LocationManager, NotificationListener registrované v onResume() bez volání unregister v onPause() drží Activity. Podobně: RxJava Disposable nepřidaný do CompositeDisposable a korutina spuštěná přes GlobalScope.
Používejte Lifecycle-aware komponenty: observe() s LifecycleOwner se automaticky odhlašuje při onDestroy(). Pro RxJava — viewLifecycleOwner.lifecycle.addObserver s DisposableObserver. Pro korutiny — lifecycleScope.launch() je vázán na životní cyklus.
// automatické odhlášení přes Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// korutiny s lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmap zabírá významné množství paměti haldy: jeden FullHD-bitmap — 1920 × 1080 × 4 bajty = 8.3 MB. Pokud se Bitmap vytváří pro každý prvek seznamu a nevolá se recycle() při skrytí, paměť rychle dochází. Na starých verzích Androidu (před 3.0) byl Bitmap uložen v nativní paměti, ale na moderních — na haldě Dalvik/ART, a GC jej může uvolnit pouze pokud neexistuje strong reference.
Používejte Glide nebo Coil pro načítání obrázků — tyto knihovny spravují cache a recycle automaticky. Pokud pracujete přímo s Bitmap, volejte bitmap.recycle() pro velké obrázky, které se již nezobrazují, a používejte inSampleSize pro načítání zmenšených kopií.
Fragment má dva životní cykly: samotného Fragmentu a jeho View. Po onDestroyView() je View strom zničen, ale samotný Fragment může zůstat v paměti, pokud existuje externí reference. Typická chyba — uchovávání reference na Fragment v adaptéru ViewPager nebo v navigačním grafu, která se při zničení nevyčistí.
Nikdy neuchovávejte referenci na Fragment v polích dlouhožijících objektů. Používejte childFragmentManager pro vnořené fragmenty a observe() s LifecycleOwner pro přenos dat mezi nimi. ViewPager2 vyřešil tento problém na úrovni API: FragmentTransactionAdapter správně spravuje životní cyklus.
Odhalení úniku vyžaduje ověření dvou skutečností: paměť se po expected lifetime nevrací a počet objektů určitého typu roste bez snižování. Proces diagnostiky zahrnuje tři fáze.
První fáze — vizuální kontrola přes Memory Profiler v Android Studio. Otevřete kartu Memory, proveďte cílovou akci (otevřete a zavřete obrazovku), stiskněte GC (Garbage Collection) a sledujte, zda se paměť vrátila na původní úroveň. Pokud po 3–4 cyklech otevření-zavření paměť stabilně roste — došlo k úniku.
Druhá fáze — pořízení Heap Dump. V Memory Profileru stiskněte Dump Java Heap. Získaný soubor .hprof otevřete v Android Studio: uvidíte všechny objekty na haldě s velikostmi a referencemi. Hledejte třídy, jejichž počet by měl být po zavření obrazovky nulový. Například MainActivity s počtem 2 po zavření — zřejmý únik.
Třetí fáze — analýza Retained Size a GC Root. V Android Studio analyzujte Retained Size: kolik paměti se uvolní, pokud tento objekt smažete. Cesta od GC Root k objektu ukazuje, co jej drží: Static field → HashMap → Activity — a vidíte bod úniku. Panel Reference widget zobrazuje všechny držitele objektu.
Čtyři nástroje pokrývají vyhledávání úniků od automatické detekce po hloubkovou analýzu Heap Dump.
| Nástroj | Metoda | Formát výsledků |
|---|---|---|
| LeakCanary | Automatické monitorování | Heap Dump + stack trace úniku |
| Android Memory Profiler | Ruční monitorování | Graf paměti + Heap Dump |
| MAT (Eclipse) | Hloubková analýza | Zpráva Dominator Tree + cesta GC Root |
| Perfetto | System-wide trasování | Časová osa + nativní paměť |
LeakCanary — must-have pro každý Android projekt. Automaticky detekuje úniky po skončení životního cyklu Activity/Fragment a ukazuje přesné místo úniku se stack trace. Integrace: jeden řádek v build.gradle. LeakCanary 2.x nevyžaduje ruční inicializaci — automaticky registruje Application Watcher.
Prevence úniků je zabudována do procesu vývoje prostřednictvím sady pravidel a nástrojů kontrolujících kód v každé fázi.
Nikdy neuchovávejte referenci na Activity, Fragment nebo View ve statickém poli, singletonu nebo dlouhožijícím objektu. Pokud se referenci nelze vyhnout — použijte WeakReference nebo ukládejte data přes ViewModel, který žije přesně tak dlouho, jak je potřeba, a nedrží View přímo.
ViewModel a LiveData z Android Architecture Components řeší problém životního cyklu na úrovni architektury. ViewModel přežije otočení obrazovky a neobsahuje reference na View. LiveData automaticky odhlašuje observer při onDestroy(). Používejte je místo ručního předplatného systémových služeb.
Při code review věnujte pozornost: statickým polím s typy Context/View, anonymním třídám, lambdám uzavírajícím Activity, ručním předplatným, RxJava disposable bez composite, ukládání Fragment přes Bundle. V Kotlin navíc kontrolujte korutiny na launch bez vazby na životní cyklus.
LeakCanary může pracovat jako součást testovacího pipeline: spouštějte akceptační testy s LeakCanary a označte build jako neúspěšný, pokud je nalezen únik. To zabraňuje pronikání úniků do produkce. Doplňte kontrolu Android Lint s pravidlem StaticFieldLeak — nachází potenciální úniky na úrovni statické analýzy.
// LeakCanary v testech
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // fail pokud je únik
}
}
Často kladené otázky
Únik je příčina a OutOfMemoryError je následek. Jeden únik nevede k OOM, ale nahromadění desítek úniků vyčerpá Heap. OOM je fatální výjimka a únik je vzor, který k ní v průběhu času vede.
Přes Android Memory Profiler: otevírejte a zavírejte obrazovku 5krát, po každém zavření zavolejte GC. Pokud se paměť nevrací na základní úroveň — došlo k úniku. Pořiďte Heap Dump a najděte v seznamu třídu Activity, jejíž počet je větší než 0 po zavření.
Částečně. Kotlin řeší problém null-safety, ale nespravuje strong references. Korutiny s lifecycleScope a viewModelScope zabraňují únikům z úloh na pozadí a sealed class a data class snižují počet stavů vedoucích k únikům. Hlavní ochrana — architektonické vzory, nikoli funkce jazyka.
LeakCanary občas dává false positive: objekt může být dočasně držen systémem (např. InputMethodManager drží poslední View). Zkontrolujte ručně: pokud Retained Size < 1 KB a GC Root je systémová služba, pravděpodobně se jedná o falešný poplach.
Ne. Úniky jsou možné na jakékoli platformě s GC: iOS (Swift/Objective-C), Flutter (Dart), webové prohlížeče (JavaScript). Mechanizmy jsou stejné — strong reference od GC Root. Na iOS ARC automaticky spravuje paměť, ale retain cycle mezi objekty vytváří stejný únik.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také