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.
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.
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();
}
...
}
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
}
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();
}
...
}
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);
…
}
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.
cache.remove('key'); // Remove an object
cache.clear(); // Clear the entire cache
final weakCache = WeakMap<Key, Value>();
weakCache[key] = BigObject(); // Doesn’t prevent garbage collection
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;
}
final imageCache = PaintingBinding.instance.imageCache;
imageCache.clear(); // Clear the image cache
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();
}
...
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();
}
...
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,
.