AlanJereb.com
Programmazione

Flutter – Rilevamento delle perdite di memoria – Linee guida

Mar 22nd, 2025

Questo è il secondo articolo della serie

Flutter – Rilevamento delle perdite di memoria

. Se ti sei perso il primo e vuoi saperne di più sul funzionamento interno del Garbage Collector di Dart, puoi farlo

.

In questo articolo, fornirò esempi di codice su come liberare manualmente oggetti che il Garbage Collector di Dart non gestirà automaticamente. Cercherò di essere il più breve possibile, con un esempio per sezione.

Modelli per liberare oggetti non raccoglibili

Gli esempi coprono tutti gli oggetti non raccoglibili menzionati nel primo articolo, tranne l'ultimo, poiché la liberazione delle librerie di terze parti è legata alla documentazione della libreria selezionata.

Oggetti con metodo dispose

Molti oggetti Flutter hanno un metodo

dispose

o

cancel

. Assicurati prima di controllare se l'oggetto che potrebbe causare perdite di memoria ne ha uno. L'AnimationController utilizzato in questo esempio è uno di questi ed è legato ai ticker del framework. Se queste risorse non vengono rilasciate correttamente, possono causare perdite di memoria.

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose(); // Dispose the AnimationController
    super.dispose();
  }
...
}

Variabili globali

Le variabili globali sono accessibili in tutta l'app e non hanno una fine del ciclo di vita definita. Ecco perché devi impostarle su

null

quando non sono più necessarie.

class AppState {
  static var largeData = List<int>.filled(1000000, 0);
}

void cleanupGlobalVariables() {
  AppState.largeData = null; // Free up memory if it is no longer needed
}

Ascoltatori di eventi/sottoscrizioni

Lo scopo principale di uno stream è quello di inviare i dati dello stream quando e dove sono necessari. Non è legato a un singolo uso, e il Garbage Collector di Dart non può sapere quando i dati dello stream non sono più necessari. Annulla le sottoscrizioni agli stream quando non sono più necessarie per prevenire perdite di memoria.

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  StreamSubscription<int>? _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = someStream.listen((data) {
      print('Stream sent data: $data');
    });
  }

  @override
  void dispose() {
    _subscription?.cancel(); // Cancel the subscription
    super.dispose();
  }
...
}

Chiusure che catturano il contesto di build

Il contesto di build di Flutter (

BuildContext

) è un oggetto di breve durata e di grandi dimensioni. Non usare mai

BuildContext

all'interno di chiusure se la chiusura SOPRAVVIVE al widget. Se hai bisogno del contesto all'interno di una chiusura, assegna il contesto a una variabile e poi usa quella variabile all'interno della chiusura. Il Garbage Collector di Dart raccoglie automaticamente le variabili.

// BAD
Widget build(BuildContext context) {
  final printer = () => print(someFunctionUsingContext(context));
  usePrinter(printer);
}

// GOOD
Widget build(BuildContext context) {
  final result = someFunctionUsingContext(context); // variable gets GCed
  final printer = () => print(result);
  usePrinter(printer);
}

Cache e mappe

La situazione con le cache e le mappe è un po' simile a quella delle variabili globali. Mantengono gli oggetti anche quando non sono più necessari mantenendo riferimenti forti a essi. Assicurarsi che le cache e le mappe non causino perdite di memoria indesiderate include diversi approcci, tutti validi e dipendenti dal caso d'uso.

  • Svuotare la cache/mappa quando un oggetto o l'intera cache non è più necessaria
cache.remove('key'); // Remove an object
cache.clear(); // Clear the entire cache

  • Usare riferimenti deboli per evitare che le cache creino riferimenti forti e impediscano al Garbage Collector di Dart di pulirla. Nota che non appena nessuno usa i valori della cache (riferendosi ad essi fortemente), diventano candidati per il Garbage Collector di Dart.
final weakCache = WeakMap<Key, Value>();
weakCache[key] = BigObject(); // Doesn’t prevent garbage collection

  • Limitare la dimensione di una cache/mappa e rimuovere le voci più vecchie quando è piena.
final cache = LinkedHashMap<String, BigObject>();
void addToCache(String key, BigObject value) {
  if (cache.length >= 100) {
    cache.remove(cache.keys.first); // Remove the oldest object
  }
  cache[key] = value;
}

  • Usare il meccanismo di caching integrato di Flutter.
final imageCache = PaintingBinding.instance.imageCache;
imageCache.clear(); // Clear the image cache

Timer e callback periodici

Quando viene creato un timer, gli oggetti nel suo callback formano un riferimento forte ad esso. Quando lo scopo del timer è superato, questi riferimenti forti esistono ancora e, senza una liberazione manuale, causeranno perdite di memoria.

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  Timer _timer;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      print("Tick-tock!");
    });
  }

  @override
  void dispose() {
    _timer.cancel(); // Cancel the timer when the widget is disposed
    super.dispose();
  }
...

Risorse natives

I metodi specifici della piattaforma devono essere gestiti/liberati correttamente. Uno sviluppatore deve gestire manualmente l'acquisizione e poi il rilascio della risorsa per prevenire perdite di memoria.

class WakeLockScreen extends StatefulWidget {
  @override
  _WakeLockScreenState createState() => _WakeLockScreenState();
}

class _WakeLockScreenState extends State<WakeLockScreen> {
  static const platform = MethodChannel('com.example.wakelock');

  @override
  void initState() {
    super.initState();
    _acquireWakeLock();
  }

  Future<void> _acquireWakeLock() async {
    try {
      await platform.invokeMethod('acquireWakeLock');
    } on PlatformException catch (e) {
      print("Failed to acquire WakeLock: ${e.message}");
    }
  }

  Future<void> _releaseWakeLock() async {
    try {
      await platform.invokeMethod('releaseWakeLock');
    } on PlatformException catch (e) {
      print("Failed to release WakeLock: ${e.message}");
    }
  }

  @override
  void dispose() {
    _releaseWakeLock(); // Release the WakeLock when the widget is disposed
    super.dispose();
  }
...

Il tuo codice sta ancora perdendo memoria?

Se credi di aver liberato tutti gli oggetti non raccoglibili, ma la tua app sta ancora perdendo memoria, è il momento di usare strumenti che ti aiutano a rilevare dove la tua app sta perdendo memoria. Prima di tutto, vediamo il più semplice,

.