AlanJereb.com
Programmation

Flutter – Détection des fuites de mémoire – Guides

Mar 22nd, 2025

Ceci est le deuxième article de la série

Flutter – Détection des fuites de mémoire

. Si vous avez manqué le premier et que vous souhaitez en savoir plus sur le fonctionnement interne du Garbage Collector de Dart, vous pouvez le faire

.

Dans cet article, je vais fournir des exemples de code sur la manière de libérer manuellement les objets que le Garbage Collector de Dart ne gérera pas automatiquement. Je vais essayer d'être aussi concis que possible, avec un exemple par section.

Modèles pour libérer les objets non collectables

Les exemples couvrent tous les objets non collectables référencés dans le premier article, sauf le dernier, car la libération des bibliothèques tierces est liée à la documentation de la bibliothèque sélectionnée.

Objets avec méthode dispose

De nombreux objets Flutter ont une méthode

dispose

ou

cancel

. Assurez-vous d'abord de vérifier si votre objet potentiellement fuiteur en a une. L'AnimationController utilisé dans cet exemple en est un et est lié aux tickers du framework. Si ces ressources ne sont pas correctement libérées, elles peuvent entraîner des fuites de mémoire.

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();
  }
...
}

Variables globales

Les variables globales sont accessibles dans toute l'application et n'ont pas de fin de cycle de vie définie. C'est pourquoi vous devez les définir sur

null

lorsqu'elles ne sont plus nécessaires.

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

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

Écouteurs d'événements/abonnements

Le but principal d'un stream est de transmettre les données du stream quand et où elles sont nécessaires. Il n'est pas lié à une seule utilisation, et le Garbage Collector de Dart ne peut pas savoir quand les données du stream ne sont plus nécessaires. Annulez les abonnements aux streams lorsqu'ils ne sont plus nécessaires pour éviter les fuites de mémoire.

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();
  }
...
}

Fermetures capturant le contexte de build

Le contexte de build de Flutter (

BuildContext

) est un objet de courte durée et de grande taille. N'utilisez jamais

BuildContext

à l'intérieur de fermetures si la fermeture SURVIT au widget. Si vous avez besoin du contexte dans une fermeture, assignez le contexte à une variable et utilisez ensuite cette variable à l'intérieur de la fermeture. Le Garbage Collector de Dart collecte automatiquement les variables.

// 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);
}

Caches et cartes

La situation avec les caches et les cartes est un peu similaire à celle des variables globales. Elles conservent les objets même lorsque vous n'en avez plus besoin en maintenant des références fortes vers eux. S'assurer que les caches et les cartes ne causent pas de fuites de mémoire indésirables inclut plusieurs approches, toutes viables et dépendantes de votre cas d'utilisation.

  • Vider le cache/carte lorsqu'un objet ou l'ensemble du cache n'est plus nécessaire.
cache.remove('key'); // Remove an object
cache.clear(); // Clear the entire cache

  • Utiliser des références faibles pour empêcher les caches de créer des références fortes et empêcher le Garbage Collector de Dart de le nettoyer. Notez que dès que personne n'utilise les valeurs du cache (en y faisant référence fortement), elles deviennent candidates pour le Garbage Collector de Dart.
final weakCache = WeakMap<Key, Value>();
weakCache[key] = BigObject(); // Doesn’t prevent garbage collection

  • Limiter la taille d'un cache/carte et supprimer les entrées les plus anciennes lorsqu'il est plein.
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;
}

  • Utiliser le mécanisme de cache intégré de Flutter.
final imageCache = PaintingBinding.instance.imageCache;
imageCache.clear(); // Clear the image cache

Minuteries et rappels périodiques

Lorsqu'une minuterie est créée, les objets dans son callback forment une référence forte à elle. Lorsque l'objectif de la minuterie est dépassé, ces références fortes existent toujours et, sans libération manuelle, elles causeront des fuites de mémoire.

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();
  }
...

Ressources natives

Les méthodes spécifiques à la plateforme doivent être gérées/libérées correctement. Un développeur doit gérer manuellement l'acquisition puis la libération de la ressource pour éviter les fuites de mémoire.

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();
  }
...

Votre code fuit-il encore ?

Si vous pensez avoir libéré tous les objets non collectables, mais que votre application fuit encore, il est temps d'utiliser des outils qui vous aident à détecter où votre application fuit. Commençons par le plus simple,

.