AlanJereb.com
Programmation

Flutter – Détection des fuites de mémoire – Dart DevTools

Apr 12th, 2025

Il s'agit du troisième article de blog de la série

Flutter – Détection de fuites mémoire

. Si vous tombez sur cet article par hasard et souhaitez commencer depuis le début, cliquez

.

Cet article couvrira la méthode la plus simple et la plus recommandée pour détecter les fuites mémoire dans Flutter, à savoir l'utilisation de l'outil Memory view de Dart DevTools. Ce sujet est largement traité dans la documentation de Flutter, les articles de blog et les vidéos YouTube, mais cette série ne serait pas complète sans lui.

Configuration du code

La meilleure façon d'apprendre une nouvelle compétence et de retenir les connaissances acquises est de l'essayer soi-même, pas seulement de lire à ce sujet. Si votre application ne fuit pas et que vous apprenez juste à détecter les fuites mémoire dans Flutter, je vous encourage vivement à utiliser ma configuration de code pour suivre. Sinon, utilisez votre application qui fuit.

Le code ci-dessous est une application Flutter simple à deux pages avec un bouton qui crée des minuteries non collectables.

import 'dart:async';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Memory Leaks demo app',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  void _induceMemoryLeak() {
    for (int i = 0; i < 100000; i++) {
      Timer.periodic(const Duration(seconds: 1), (timer) {
        // do nothing
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text("Memory Leaks demo page"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CustomTextButton(onPressed: _induceMemoryLeak, text: "Add 100.000 timers"),
            const SizedBox(height: 30),
            CustomTextButton(
              onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const SecondPage())),
              text: "Navigate",
            ),
          ],
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  const SecondPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text("Memory Leaks demo page - 2")),
      body: Center(
        child: CustomTextButton(
          onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const HomePage())),
          text: "Navigate back",
        ),
      ),
    );
  }
}

class CustomTextButton extends StatelessWidget {
  const CustomTextButton({super.key, required this.text, required this.onPressed});

  final String text;
  final Function() onPressed;

  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: TextButton.styleFrom(
        foregroundColor: Colors.white,
        backgroundColor: Colors.teal,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
      onPressed: onPressed,
      child: Text(text),
    );
  }
}

Une application à deux pages avec des minuteries qui fuient

Détection des fuites de mémoire avec Dart DevTools

Première chose : vous ne devez JAMAIS chercher des fuites mémoire en exécutant l'application Flutter en mode debug, car la mémoire absolue utilisée peut être plus élevée en mode debug. Pour des résultats plus précis, utilisez le mode

profile

.

Étapes de détection :

  • basculez vers l'onglet Memory view et sélectionnez l'onglet Diff Snapshots
  • créez un instantané du tas mémoire
  • utilisez l'application
  • créez un autre instantané du tas mémoire
  • comparez pour trouver les objets qui fuient

Étape 1 : Basculez vers l'onglet Memory view et sélectionnez l'onglet Diff Snapshots

Lorsque votre application s'exécute en mode profile, ouvrez Dart DevTools et basculez vers l'onglet

Memory view

. Cela ouvre tous les outils liés à la mémoire que Dart DevTools propose.

Ensuite, cliquez sur l'onglet

Diff Snapshots

. Cet outil vous permet de prendre des instantanés du tas mémoire au cours de la vie de l'application et de les comparer entre eux. Si vous vous souvenez du

de cette série, les objets qui fuient ne sont pas collectés par le garbage collector, et en théorie, vous devriez les attraper ici.

Dart DevTools - Onglet Memory view

Dart DevTools - Onglet Memory view

Étape 2 : Créez un instantané du tas mémoire

Dès que votre application se stabilise, vous devez créer un instantané du tas mémoire. Celui-ci servira de base pour la comparaison de la mémoire du tas.

Dart DevTools - Comment créer un snapshot du tas mémoire

Dart DevTools - Comment créer un snapshot du tas mémoire

Étape 3 : Utilisez l'application

Comme vous n'êtes pas vraiment sûr de ce qui cause vos fuites mémoire, cette étape consiste à essayer de provoquer des fuites mémoire. Vous devez utiliser votre application de la même manière que vos utilisateurs finaux l'utilisent ou l'utiliseront. Quelque chose pendant son fonctionnement cause vos fuites mémoire. Si vous suivez cet article en utilisant le code que j'ai fourni, c'est le moment d'appuyer sur le bouton

Add 100.000 timers

puis sur le bouton

Navigate

pour accéder à la page suivante. En revenant à la page d'origine, le garbage collector aurait dû collecter les minuteries si elles avaient été correctement libérées, mais ce n'est pas le cas.

Application qui fuit

Application qui fuit

Étape 4 : Créez un autre instantané du tas mémoire

Maintenant, votre application fuit probablement, et il est temps de créer un nouvel instantané du tas mémoire qui sera comparé à l'instantané initial. Avant de créer un instantané, forcez le Garbage Collection en appuyant sur le bouton

GC

dans l'onglet Memory View de Dart DevTools.

Dart DevTools - Forcer le garbage collection et créer un nouveau snapshot

Dart DevTools - Forcer le garbage collection et créer un nouveau snapshot

Étape 5 : Comparez pour trouver les objets qui fuient

Avant de comparer les instantanés, assurez-vous de définir les filtres de classe pour afficher tous les types d'objets. Ce n'est qu'ainsi que vous êtes "sûr" de voir tous les objets.

Dart DevTools - Filtrage des types d'objets

Dart DevTools - Filtrage des types d'objets

Et enfin, vous comparez votre dernier instantané du tas mémoire avec l'original.

Dart DevTools - Comparaison des snapshots

Dart DevTools - Comparaison des snapshots

En observant la différence entre les deux instantanés, nous pouvons voir que, dans mon cas, la fuite était bien causée par des minuteries non collectées par le garbage collector. Vous pouvez voir le nombre de minuteries créées, qu'aucune n'a été libérée de la mémoire et qu'elles occupent désormais 7,6 Mo de mémoire supplémentaire.

Que faire si vous avez trouvé le coupable ?

Si vous avez eu la chance de trouver votre ou vos coupables, il est temps de surveiller l'objet ou les objets et d'essayer de trouver l'origine de la fuite dans votre application. Si vous utilisez mon code, vous connaissez déjà l'origine, mais dans la vraie vie, l'origine des fuites sera plus difficile à trouver. Je vous présente l'onglet

Trace Instances

.

Après être passé à l'onglet Trace Instances, vous devez définir des filtres pour les classes d'objets que vous souhaitez observer et cocher la case

Trace

pour que le traçage commence. Vous devez ensuite utiliser votre application comme vous l'avez fait dans l'onglet

Diff Snapshots

entre vos deux instantanés du tas mémoire. Lorsque vous êtes assez certain que la fuite a été introduite, appuyez sur le bouton

Refresh

.

Dart DevTools - Traçage des classes qui fuient

Dart DevTools - Traçage des classes qui fuient

Lorsque le delta de la classe change, il est temps de commencer l'analyse. Cliquez sur la classe puis sur le bouton

Expand All

pour développer les traces. Lisez attentivement l'arbre des appels pour trouver l'origine de votre fuite.

Dart DevTools - Origine des temporisateurs qui fuient trouvée

Dart DevTools - Origine des temporisateurs qui fuient trouvée

Votre code fuit-il toujours ?

L'outil ci-dessus n'est pas omnipotent. Votre application peut encore fuir, et l'outil ne vous a pas aidé à trouver la source. Cela arrive pour diverses raisons, comme :

  • Rétentions indirectes – Les objets conservés via des fermetures, des rappels ou des internes de framework peuvent ne pas montrer de chemins de rétention clairs.
  • Ressources non fermées – Les StreamSubscriptions, AnimationControllers ou ScrollControllers oubliés peuvent fuir sans traces apparentes.
  • Références statiques/globales – Les objets stockés dans des variables statiques ou des caches globaux n'apparaîtront pas comme des fuites—ils sont intentionnellement retenus mais pourraient être involontaires dans votre logique.
  • Plugins tiers – Les fuites dans les plugins ou les bibliothèques peuvent ne pas être visibles dans le suivi des instances de votre application.
  • Graphes d'objets complexes – Si un objet est retenu à travers plusieurs couches (par exemple, un mélange de widgets, de providers et de streams), Dart DevTools pourrait ne pas mettre en évidence la cause racine.

N'oubliez pas que si les techniques ci-dessus n'ont pas aidé, votre fuite pourrait être relativement subtile – il est temps d'enfiler votre chapeau de détective et d'essayer le prochain outil de ma ceinture utilitaire, le Dart Leak Tracker. L'outil est encore en développement et n'est pas officiellement sorti, mais je vous montrerai comment l'utiliser dans le

prochain article

de la série

(à venir)

.