Diagnosticarea și Profilarea Memoriei în Aplicații Dart: Detectarea Scurgerilor de Memorie (Memory Leaks), WeakReferences și Dart DevTools în Dart

Ghid practic avansat de optimizare și diagnosticare a memoriei în Dart: funcționarea Generational Garbage Collector-ului, identificarea retain cycle-urilor și a scurgerilor de memorie în StreamSubscriptions/clojures, utilizarea WeakReference și Finalizer, și analiza heap-ului cu Dart DevTools.

Sintaxă
final weakRef = WeakReference(heavyObject); final finalizer = Finalizer<String>((key) => ...);

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).

Mecanismul Garbage Collector-ului Generațional în Dart
# 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>:

Implementare: Cache cu Memorie Slabă și Eliberare Automată
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:

  1. Lansați aplicația în modul profile: dart run --profile sau flutter run --profile.
  2. Deschideți Dart DevTools și navigați la tab-ul Memory.
  3. Efectuați acțiunea suspectată de memory leak de 5-10 ori (de exemplu: deschiderea și închiderea repetată a unui ecran).
  4. Apăsați butonul Take Snapshot și comutați la vizualizarea Diff Snapshots.
  5. 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).