Scurgerea de memorie (Memory Leak) — situația în care aplicația păstrează referințe către obiecte care nu mai sunt necesare, împiedicând garbage collector-ul să elibereze occupied memory. Conform datelor LeakCanary, chiar și în aplicații bine scrise se întâlnesc 3–5 scurgeri la 10 000 de linii de cod. Fiecare scurgere reduce treptat memoria disponibilă, ducând la încetiniri și OutOfMemoryError.
Puncte cheie
Scurgerea de memorie (Memory Leak) — este situația în care un obiect rămâne accesibil printr-un lanț de referințe puternice (Strong Reference), deși logic nu mai este necesar aplicației. Garbage collector-ul (GC) consideră un astfel de obiect viu și nu eliberează memoria ocupată de acesta. Ca rezultat, memoria heap disponibilă se reduce constant, iar frecvența pauzelor GC crește.
Spre deosebire de limbajele cu gestionare manuală a memoriei (C, C++), în Java/Kotlin scurgerea nu este un free() uitat, ci o referință uitată. Cât timp există o strong reference de la obiectul rădăcină (GC Root) până la obiectul scurs, GC îl consideră necesar. GC Root-uri tipice: câmpuri statice, fire active, stiva de apeluri, referințe globale JNI.
Pericolul scurgerilor este efectul lor cumulativ. O scurgere de 100 KB nu se observă, dar 100 de astfel de scurgeri ocupă 10 MB, iar aplicația începe să încetinească din cauza GC-urilor frecvente. Masa critică de scurgeri duce la OutOfMemoryError și crash-ul aplicației. Simptomele scurgerii: creșterea constantă a consumului de memorie pe graficul Profiler, pauze frecvente GC cu STW (Stop The World) și scăderea performanței UI.
Cinci tipuri de scurgeri acoperă 95% din cazurile în dezvoltarea mobilă. Fiecare are cauza și modelul său caracteristic în cod.
Cea mai cunoscută scurgere pe Android — păstrarea unei referințe statice la Activity sau Context. Cod tipic: un câmp static Activity care nu se anulează la onDestroy(). Cât timp câmpul static este viu, întregul Activity cu arborele său View, care poate ocupa 1–10 MB, rămâne viu. Aceasta este o scurgere clasică pe care LeakCanary o găsește în primul rând.
Soluție: nu păstrați niciodată Activity sau Context în câmpuri statice. Folosiți Application Context pentru singleton-uri care supraviețuiesc Activity-ului. Dacă aveți nevoie de o referință la Activity — folosiți WeakReference<Activity>.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
Clasele anonime și clasele imbricate non-statice păstrează implicit o referință către clasa care le conține. Un Runnable transmis către Handler care se execută după onDestroy() menține întregul Activity. Un callback Retrofit care închide Activity face același lucru. Acesta este cel mai perfid tip de scurgere — referința implicită nu este vizibilă în cod.
Expresiile object și lambda-urile din Kotlin capturează și ele referințe către clasa externă. Faceți clasele imbricate statice (sau top-level în Kotlin) și transmiteți referințele externe prin WeakReference. Pentru lambda-uri, folosiți abordarea Lifecycle-aware cu viewLifecycleOwner.
Abonarea la servicii sistem fără dezabonare — o scurgere directă. SensorManager, LocationManager, NotificationListener înregistrate în onResume() fără apelarea unregister în onPause() mențin Activity-ul. Similar: RxJava Disposable neadăugat la CompositeDisposable și corutină lansată prin GlobalScope.
Folosiți componente Lifecycle-aware: observe() cu LifecycleOwner se dezabonează automat la onDestroy(). Pentru RxJava — viewLifecycleOwner.lifecycle.addObserver cu DisposableObserver. Pentru corutine — lifecycleScope.launch() este legat de ciclul de viață.
// dezabonare automată prin Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// corutine cu lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmap ocupă o cantitate semnificativă de memorie heap: un FullHD-bitmap — 1920 × 1080 × 4 biți = 8.3 MB. Dacă se creează un Bitmap pentru fiecare element al listei și nu se apelează recycle() la ascundere, memoria se epuizează rapid. Pe versiunile vechi de Android (înainte de 3.0), Bitmap era stocat în memoria native, dar pe cele moderne — în heap-ul Dalvik/ART, iar GC îl poate elibera doar dacă nu există strong reference.
Folosiți Glide sau Coil pentru încărcarea imaginilor — aceste biblioteci gestionează caching-ul și recycle-ul automat. Dacă lucrați direct cu Bitmap, apelați bitmap.recycle() pentru imaginile mari care nu mai sunt afișate și folosiți inSampleSize pentru încărcarea copiilor reduse.
Fragment are două cicluri de viață: al Fragmentului propriu-zis și al View-ului său. După onDestroyView(), arborele View este distrus, dar Fragmentul însuși poate rămâne în memorie dacă există o referință externă. Eroare tipică — păstrarea referinței la Fragment în adaptorul ViewPager sau în graficul de navigare care nu se curăță la distrugere.
Nu păstrați niciodată referința la Fragment în câmpurile obiectelor longevive. Folosiți childFragmentManager pentru fragmente imbricate și observe() cu LifecycleOwner pentru transmiterea datelor între ele. ViewPager2 a rezolvat această problemă la nivel de API: FragmentTransactionAdapter gestionează corect ciclul de viață.
Depistarea scurgerii necesită verificarea a două fapte: memoria nu revine după expected lifetime și numărul de obiecte de un anumit tip crește fără a scădea. Procesul de diagnosticare include trei etape.
Prima etapă — verificarea vizuală prin Memory Profiler în Android Studio. Deschideți fila Memory, executați acțiunea țintă (deschideți și închideți ecranul), apăsați GC (Garbage Collection) și urmăriți dacă memoria revine la nivelul inițial. Dacă după 3–4 cicluri de deschidere-închidere memoria crește constant — există o scurgere.
A doua etapă — realizarea unui Heap Dump. În Memory Profiler apăsați Dump Java Heap. Fișierul .hprof obținut deschideți-l în Android Studio: veți vedea toate obiectele din heap cu dimensiuni și referințe. Căutați clasele al căror număr ar trebui să fie zero după închiderea ecranului. De exemplu, MainActivity cu numărul 2 după închidere — o scurgere evidentă.
A treia etapă — analiza Retained Size și GC Root. În Android Studio analizați Retained Size: câtă memorie se va elibera dacă ștergeți acest obiect. Calea de la GC Root la obiect arată ce îl menține: Static field → HashMap → Activity — și vedeți punctul de scurgere. Panoul Reference widget arată toți deținătorii obiectului.
Patru instrumente acoperă detectarea scurgerilor de la depistarea automată până la analiza profundă a Heap Dump.
| Instrument | Metodă | Format rezultate |
|---|---|---|
| LeakCanary | Monitorizare automată | Heap Dump + stack trace al scurgerii |
| Android Memory Profiler | Monitorizare manuală | Grafic memorie + Heap Dump |
| MAT (Eclipse) | Analiză profundă | Raport Dominator Tree + cale GC Root |
| Perfetto | Trasare system-wide | Linie temporală + memorie native |
LeakCanary — must-have pentru orice proiect Android. Detectează automat scurgerile după încheierea ciclului de viață al Activity/Fragment și arată locul exact al scurgerii cu stack trace. Integrare: o singură linie în build.gradle. LeakCanary 2.x nu necesită inițializare manuală — înregistrează automat Application Watcher.
Prevenirea scurgerilor este integrată în procesul de dezvoltare printr-un set de reguli și instrumente care verifică codul în fiecare etapă.
Niciodată nu păstrați referința la Activity, Fragment sau View într-un câmp static, singleton sau obiect longeviv. Dacă nu se poate evita referința — folosiți WeakReference sau stocați datele prin ViewModel, care trăiește exact atât cât trebuie și nu menține View-ul direct.
ViewModel și LiveData din Android Architecture Components rezolvă problema ciclului de viață la nivel de arhitectură. ViewModel supraviețuiește rotației ecranului și nu conține referințe către View. LiveData dezabonează automat observer-ul la onDestroy(). Folosiți-le în locul abonării manuale la servicii sistem.
În code review acordați atenție: câmpurilor statice cu tipuri Context/View, claselor anonime, lambda-urilor care închid Activity, abonamentelor manuale, RxJava disposable fără composite, stocării Fragment prin Bundle. În Kotlin verificați suplimentar corutinele pentru launch fără legătură la ciclul de viață.
LeakCanary poate funcționa ca parte a pipeline-ului de testare: rulați teste de acceptare cu LeakCanary și marcați build-ul ca eșuat dacă se găsește o scurgere. Aceasta previne intrarea scurgerilor în producție. Completați verificarea cu Android Lint cu regula StaticFieldLeak — aceasta găsește scurgeri potențiale la nivel de analiză statică.
// LeakCanary în teste
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // fail dacă există scurgere
}
}
Întrebări frecvente
Scurgerea este cauza, iar OutOfMemoryError este efectul. O singură scurgere nu duce la OOM, dar acumularea a zeci de scurgeri epuizează heap-ul. OOM este o excepție fatală, iar scurgerea este un model care duce la ea în timp.
Prin Android Memory Profiler: deschideți și închideți ecranul de 5 ori, după fiecare închidere apelați GC. Dacă memoria nu revine la nivelul de bază — există o scurgere. Faceți un Heap Dump și găsiți în listă clasa Activity a cărei cantitate este mai mare de 0 după închidere.
Parțial. Kotlin rezolvă problema null-safety, dar nu gestionează strong references. Corutinele cu lifecycleScope și viewModelScope previn scurgerile din sarcinile de fundal, iar sealed class și data class reduc numărul de stări care duc la scurgeri. Protecția principală o constituie modelele arhitecturale, nu funcțiile limbajului.
LeakCanary uneori dă false positive: un obiect poate fi reținut temporar de sistem (de exemplu, InputMethodManager reține ultimul View). Verificați manual: dacă Retained Size < 1 KB și GC Root este un serviciu de sistem, probabil este o alarmă falsă.
Nu. Scurgerile sunt posibile pe orice platformă cu GC: iOS (Swift/Objective-C), Flutter (Dart), browsere web (JavaScript). Mecanismele sunt aceleași — strong reference de la GC Root. Pe iOS, ARC gestionează automat memoria, dar retain cycle între obiecte creează aceeași scurgere.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și