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