Scurgerea de memorie (memory leak) — situația în care aplicația nu eliberează memoria ocupată de obiecte care nu mai sunt necesare. În dezvoltarea mobilă, acest lucru este deosebit de critic: heap-ul limitat și lipsa swap-ului duc la OutOfMemoryError și la prăbușirea aplicației. Conform Purdue University (2022), 35% dintre aplicațiile Android din Google Play conțin cel puțin o scurgere de memorie. Vom analiza scenariile tipice, instrumentele de diagnosticare și metodele de eliminare.
Principalele puncte
Scurgerea de memorie — este situația în care memoria alocată nu este returnată sistemului după ce obiectul nu mai este necesar programului. Colectorul de gunoi consideră un astfel de obiect viu, deoarece către el duce un lanț activ de referințe de la GC Root.
În Java/Kotlin, colectorul de gunoi funcționează automat, dar nu poate determina că obiectul nu este necesar logic dacă există o referință tehnică la el. Dezvoltatorul trebuie să întrerupă explicit conexiunile inutile. În Swift/Objective-C, ARC numără automat referințele, dar retain cycles blochează resetarea contorului.
Principalul pericol al scurgerilor — efectul cumulativ. Fiecare scurgere consumă o cantitate mică de memorie, dar la tranziții multiple între ecrane (rotiri ale ecranului, deschiderea/închiderea Activity), scurgerile se acumulează până când limita heap-ului este epuizată.
Scurgerea — obiectul este inaccesibil codului, dar nu a fost șters de GC. Umflarea — obiectul este necesar logic, dar este stocat în cantitate excesivă. Exemplu de umflare: un cache de imagini de 100 MB cu un set de lucru de 30 MB. Ambele probleme duc la OOM, dar cauzele și metodele de tratament sunt diferite.
ART (Android Runtime) utilizează colectarea generațională a gunoiului cu concurrent compaction. Memoria este împărțită în generația tânără (Young), cea veche (Old) și obiecte mari (Large). Obiectele care au supraviețuit mai multor cicluri GC sunt mutate în Old generation, unde colectarea are loc mai rar — aceasta accelerează ciclurile obișnuite.
GC pornește când heap-ul atinge un anumit prag de ocupare (de obicei 75-85%). În timpul GC, toate firele de execuție ale aplicației sunt suspendate (STW — Stop The World). Cu cât sunt mai multe obiecte vii, cu atât pauza este mai lungă. Scurgerile măresc numărul de obiecte vii, prelungind pauzele GC.
Colectorul determină obiectele vii parcurgând graful de la GC Roots: câmpuri statice, variabile de stivă ale firelor active, referințe JNI. Orice obiect accesibil prin referințe de la aceste rădăcini este considerat viu — chiar dacă dezvoltatorul știe că nu mai este necesar.
// Exemplu: colecție statică ca GC Root — scurgere permanentă
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference nu blochează GC — comportament corect
}
}
WeakReference rezolvă problema: GC ignoră referințele slabe la determinarea obiectelor vii. Dacă asupra unui obiect au rămas doar referințe slabe, acesta va fi colectat în următorul ciclu GC.
Activity Context — cel mai răspândit scenariu de scurgeri în Android. Dacă un singleton, un câmp static sau un serviciu de lungă durată stochează o referință la Activity Context, întreaga Activity cu toate View-urile nu poate fi colectată de GC. Soluție: utilizați Application Context pentru obiectele de lungă durată.
Handler și mesajele trimise — Handler.postDelayed(runnable, delay) plasează un mesaj în coada Main Looper. Dacă Activity este distrusă înainte de expirarea întârzierii, mesajul este încă în coadă și menține referința prin Runnable → clasă anonimă → clasă exterioară (Activity).
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // obligatoriu: curățați coada
super.onPause()
}
}
Inner Classes — o clasă interioară non-statică are o referință implicită la instanța clasei exterioare. Dacă clasa exterioară este Activity, iar clasa interioară a fost transmisă undeva în afară (de exemplu, în RecyclerView.Adapter), Activity nu poate fi colectată.
Android Studio Memory Profiler — instrumentul încorporat pentru monitorizarea heap-ului în timp real. Afișează graficul memoriei ocupate, numărul de alocări și obiecte pe tipuri. Permite înregistrarea unui heap dump și exportul în format HPROF pentru analiză în MAT.
Eclipse MAT (Memory Analyzer Tool) — analizor desktop de heap dump. Construiește automat Leak Suspects Report, care evidențiază obiectele cu cel mai mare retained size și sugerează un posibil lanț GC root pentru fiecare obiect suspect.
Xcode Memory Graph Debugger — pentru iOS. Suspendă aplicația și vizualizează graful obiectelor. Retain cycles sunt evidențiate cu roșu, se poate face clic pe orice obiect pentru a vedea retain count și referințele sale.
| Instrument | Capabilități | Complexitate |
|---|---|---|
| Memory Profiler | Grafic în timp real, heap dump, Urmărirea alocării obiectelor | Scăzută |
| Eclipse MAT | Arborele dominator, Suspecți de scurgere, interogări OQL | Medie |
| LeakCanary | Detectare automată, urma scurgerii în notificare | Minimă |
| Xcode Memory Graph | Graf vizual al retain cycles, listă de obiecte vii | Scăzută |
Conform Uber Engineering Blog, implementarea profilării automate a memoriei (LeakCanary + analiza heap dump) în pipeline-ul CI/CD reduce numărul de incidente legate de memorie în producție cu 60% în decurs de 3 luni.
Înlocuirea Context — dacă obiectul trăiește mai mult decât Activity, utilizați applicationContext. Toate obiectele de lungă durată (singletoni, repository-uri, database helpers) trebuie să primească Application Context, nu Activity Context. Excepție: componentele UI care au nevoie de acces la tema sau resursele specifice Activity.
Componente Lifecycle-aware — utilizarea LifecycleObserver, DefaultLifecycleObserver sau reactivex anulează automat abonamentele la onDestroy. Android Jetpack oferă lifecycleScope și viewModelScope, care sunt curățate de evenimentul corespunzător.
Static inner class — dacă clasa interioară nu are nevoie de acces la câmpurile clasei exterioare, faceți-o static. O clasă interioară statică nu are o referință implicită la clasa exterioară. Dacă accesul este necesar, utilizați WeakReference pentru o referință explicită.
class MyActivity : AppCompatActivity() {
// ❌ Clasă interioară non-statică — referință implicită la MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Clasă interioară statică — fără referință implicită
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
În iOS, utilizați capture lists: [weak self] în închideri care pot supraviețui creatorului. Pentru delegați, utilizați referințe slabe (weak var delegate). Pentru închiderile care sunt garantat apelate doar în timpul vieții lui self, se poate utiliza [unowned self], dar cu prudență — accesarea unui obiect eliberat va cauza un crash.
Întrebări frecvente
În Android, efectuați câteva tranziții între ecrane (Activity A → B → A → B) și verificați adb shell dumpsys meminfo package_name. Dacă Total PSS crește constant și nu revine la valoarea inițială — există o scurgere. În iOS, similar: utilizați Debug Memory Graph în Xcode pentru verificare vizuală.
Da, dacă CoroutineScope nu este anulat la distrugerea componentului. O corutină lansată în GlobalScope continuă să se execute chiar și după finish() al Activity. Soluție: utilizați viewModelScope (anulat în onCleared) sau lifecycleScope (anulat în onDestroy). Pentru Scope-uri proprii, creați lifecycle-aware scopes prin LifecycleOwner.
Bitmap stochează datele pixelilor în heap-ul nativ, nu în Java heap. Aceasta înseamnă că Java GC nu vede dimensiunea reală a Bitmap-ului. Dacă Bitmap nu este apelat cu recycle() sau referința nu este anulată, memoria nativă nu va fi eliberată. Utilizați BitmapFactory cu inSampleSize pentru încărcarea copiilor reduse și Glide/Coil pentru gestionarea automată a cache-ului.
Câmpul static — este un GC Root. Trăiește cât timp clasa este încărcată (în Android — cât timp trăiește Process). Dacă un câmp static face referință la Activity, Bitmap, View sau orice alt obiect greu, acest obiect nu va fi niciodată colectat de GC. Câmpul static — o referință eternă. Soluție: stocați doar WeakReference sau anulați câmpul static în onDestroy.
ARC eliberează automat obiectele când contorul de referințe tari scade la zero. Retain cycle — singurul mod de scurgere în ARC. Utilizați întotdeauna weak pentru referințele parent→child, unde child trebuie să supraviețuiască parent-ului (delegați, data source). Pentru închideri, utilizați capture list [weak self] și verificați self pentru nil în interiorul închiderii.
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