Chiar dacă Dart este un limbaj cu gestiune automată a memoriei (*Garbage Collection*), concepția greșită că „în Dart nu pot apărea memory leaks” este cauza principală a multor aplicații Flutter care devin lente, consumă gigabytes de RAM și sunt terminate forțat de sistemul de operare prin evenimente Out-Of-Memory (OOM) pe dispozitive mobile.
O scurgere de memorie (Memory Leak) în Dart apare atunci când un obiect care nu mai este necesar aplicației rămâne ancorat într-un lanț de referințe puternice (*Retaining Path*) pornit de la o rădăcină GC (GC Root: variabile globale, fire active, callback-uri asincrone sau listeneri de evenimente neeliminați).
# Alocare Obiect Nou
# │
# ▼
# ┌─────────────────────────────────────────────────────────────┐
# │ Young Generation (Nursery / Semi-Space Copying GC) │
# │ - Alocare ultra-rapidă prin bump-pointer pe stivă/heap │
# │ - Colectare Scavenge frecventă (<1 ms) │
# │ - Obiectele care supraviețuiesc mai multor cicluri sunt: │
# └────────────────────────────┬────────────────────────────────┘
# │ (Promovare)
# ▼
# ┌─────────────────────────────────────────────────────────────┐
# │ Old Generation (Mark-Sweep-Compact Concurrent GC) │
# │ - Gestionează obiectele cu ciclu de viață lung │
# │ - Colectare concurentă în fundal fără a opri Event Loop-ul│
# └─────────────────────────────────────────────────────────────┘
Cele Mai Frecvente 3 Cauze de Memory Leaks în Aplicații Dart
| Cauză de Scurgere | Mecanism Intern | Soluție Tehnică Obligatorie |
|---|---|---|
| Abonamente Stream Neanulate | stream.listen(...) menține o referință puternică către this prin intermediul callback-ului intern. |
Stocarea instanței StreamSubscription și apelarea subscription.cancel() în dispose(). |
| Listeneri Globali / Notifiers | Adăugarea unui listener într-un singleton fără eliminare la distrugerea componentei. | Utilizarea sistematică a removeListener() la finalul ciclului de viață. |
| Cache-uri Statice Nelimitate | Colecții static final Map<K, V> în care elementele sunt doar adăugate, dar nu se elimină niciodată. |
Implementarea unui cache LRU (Least Recently Used) sau utilizarea WeakReference. |
Gestiunea Avansată cu WeakReference și Finalizer
Pentru a stoca referințe către obiecte grele (imagini, cache-uri, buffere de memorie) fără a împiedica Garbage Collector-ul să le recicleze atunci când memoria devine rară, Dart pune la dispoziție WeakReference<T> și Finalizer<T>:
import 'dart:core';
class HeavyDocument {
final String documentId;
final List rawBytes;
HeavyDocument(this.documentId, this.rawBytes);
}
class ResilientDocumentCache {
// Stocăm referințe slabe (WeakReference) către documentele grele:
// GC-ul poate curăța oricând obiectul dacă nu mai există referințe puternice către el!
final Map> _weakCache = {};
// Finalizer-ul este notificat automat de către GC după ce obiectul a fost reciclat:
late final Finalizer _finalizer;
ResilientDocumentCache() {
_finalizer = Finalizer((docId) {
print('[GC NOTIFICATION] Documentul $docId a fost colectat din memorie.');
_weakCache.remove(docId);
});
}
void put(HeavyDocument doc) {
_weakCache[doc.documentId] = WeakReference(doc);
// Înregistrăm documentul în Finalizer:
_finalizer.attach(doc, doc.documentId);
}
HeavyDocument? get(String docId) {
final weakRef = _weakCache[docId];
if (weakRef == null) return null;
final target = weakRef.target;
if (target == null) {
// Obiectul a fost colectat de Garbage Collector între timp:
_weakCache.remove(docId);
return null;
}
return target;
}
}
Ghid de Investigare a Memoriei cu Dart DevTools
Pentru detectarea rapidă a scurgerilor de memorie în mediul local sau de testare:
- Lansați aplicația în modul profile:
dart run --profilesauflutter run --profile. - Deschideți Dart DevTools și navigați la tab-ul Memory.
- Efectuați acțiunea suspectată de memory leak de 5-10 ori (de exemplu: deschiderea și închiderea repetată a unui ecran).
- Apăsați butonul Take Snapshot și comutați la vizualizarea Diff Snapshots.
- Dacă numărul de instanțe ale clasei crește continuu fără a reveni la valoarea inițială, inspectați arborele de Retaining Paths pentru a identifica exact referința rămasă activă (callback, timer sau listener static).