AlanJereb.com
Programación

Flutter – Detección de fugas de memoria – Dart DevTools

Apr 12th, 2025

Este es el tercer artículo de la serie

Flutter – Detección de fugas de memoria

. Si llegaste a este artículo por casualidad y quieres comenzar desde el principio, dirígete

.

Este artículo cubrirá la forma más simple y recomendada de detectar fugas de memoria en Flutter, que es utilizando la herramienta Memory view de Dart DevTools. Este es un tema ampliamente cubierto por la documentación de Flutter, artículos de blog y videos de YouTube, pero esta serie no estaría completa sin él.

Configuración del código

La mejor manera de aprender una nueva habilidad y retener el conocimiento adquirido es intentándolo por ti mismo, no solo leyendo sobre ello. Si tu aplicación no tiene fugas y solo estás aprendiendo sobre detección de fugas de memoria en Flutter, te insto a que uses mi configuración de código para seguir el ejemplo. De lo contrario, usa tu aplicación con fugas.

El código a continuación es una aplicación simple de Flutter de dos páginas con un botón que crea temporizadores no recolectables.

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

Una aplicación de dos páginas con temporizadores con fugas

Detección de fugas de memoria con Dart DevTools

Primero lo primero. NUNCA debes buscar fugas de memoria ejecutando la aplicación de Flutter en modo debug, ya que el uso absoluto de memoria puede ser mayor en este modo. Para resultados más precisos, usa el modo

profile

.

Pasos de detección:

  • cambia a la pestaña Memory view y selecciona la pestaña Diff Snapshots
  • crea un snapshot del montón de memoria
  • usa la aplicación
  • crea otro snapshot del montón de memoria
  • compara para encontrar los objetos con fugas

Paso 1: Cambia a la pestaña Memory view y selecciona la pestaña Diff Snapshots

Cuando tu aplicación se esté ejecutando en modo profile, abre Dart DevTools y cambia a la pestaña

Memory view

. Esto abre todas las herramientas relacionadas con la memoria que ofrece Dart DevTools.

Luego haz clic en la pestaña

Diff Snapshots

. Esta herramienta te permite tomar snapshots del montón de memoria durante el ciclo de vida de la aplicación y compararlos entre sí. Si recuerdas del

de esta serie, los objetos con fugas no son recolectados por el garbage collector, y en teoría, deberías detectarlos aquí.

Dart DevTools - Pestaña Memory View

Dart DevTools - Pestaña Memory View

Paso 2: Crea un snapshot del montón de memoria

Tan pronto como tu aplicación se estabilice, necesitas crear un snapshot del montón de memoria. Este servirá como base para la comparación de memoria.

Dart DevTools - Cómo crear un snapshot del montón de memoria

Dart DevTools - Cómo crear un snapshot del montón de memoria

Paso 3: Usa la aplicación

Como no estás completamente seguro de qué está causando tus fugas de memoria, este paso trata de inducir fugas de memoria. Debes usar tu aplicación de la misma manera que lo hacen tus usuarios finales. Algo durante su operación está causando las fugas. Si estás siguiendo este artículo usando mi código proporcionado, ahora es el momento de presionar el botón

Add 100.000 timers

y luego el botón

Navigate

para ir a la siguiente página. Al volver a la página original, el garbage collector debería haber recolectado los temporizadores si se liberaron correctamente, pero no lo hizo.

Aplicación con fugas

Aplicación con fugas

Paso 4: Crea otro snapshot del montón de memoria

Ahora, tu aplicación probablemente tiene fugas, y es hora de crear un nuevo snapshot del montón de memoria que se comparará con el snapshot inicial. Antes de hacer el snapshot, fuerza el Garbage Collection presionando el botón

GC

dentro de la pestaña Memory View de Dart DevTools.

Dart DevTools - Forzar la recolección de basura y crear un nuevo snapshot

Dart DevTools - Forzar la recolección de basura y crear un nuevo snapshot

Paso 5: Compara para encontrar los objetos con fugas

Antes de comparar los snapshots, asegúrate de configurar los filtros de clase para mostrar todos los tipos de objetos. Solo así puedes estar "seguro" de ver todos los objetos.

Dart DevTools - Filtrado de tipos de objetos

Dart DevTools - Filtrado de tipos de objetos

Finalmente, comparas tu último snapshot del montón con el original.

Dart DevTools - Comparación de snapshots

Dart DevTools - Comparación de snapshots

Observando la diferencia entre los dos snapshots, podemos ver que, en mi caso, la fuga fue causada efectivamente por temporizadores no recolectados. Puedes ver la cantidad de temporizadores creados, que ninguno fue liberado de la memoria y que ahora ocupan 7.6MB adicionales de memoria.

¿Qué hacer si encontraste al culpable?

Si tuviste la suerte de encontrar tu(s) culpable(s), es hora de monitorear el/los objeto(s) e intentar encontrar el origen de la fuga en tu app. Si usas mi código, ya conoces el origen, pero en la vida real será más difícil. Te presento la pestaña

Trace Instances

.

Al cambiar a la pestaña Trace Instances, debes configurar filtros para las clases de objetos que quieres observar y marcar la casilla

Trace

para que comience el rastreo. Luego debes usar tu app como lo hiciste en la pestaña

Diff Snapshots

entre tus dos snapshots. Cuando estés razonablemente seguro de que la fuga ocurrió, presiona

Refresh

.

Dart DevTools - Rastreo de clases con fugas

Dart DevTools - Rastreo de clases con fugas

Cuando cambie el delta de la clase, es hora de analizar. Haz clic en la clase y luego en

Expand All

para ver las trazas. Lee cuidadosamente el árbol de llamadas para encontrar el origen de tu fuga.

Dart DevTools - Origen de los temporizadores con fugas encontrado

Dart DevTools - Origen de los temporizadores con fugas encontrado

¿Tu código todavía tiene fugas?

Esta herramienta no es omnipotente. Tu app podría seguir con fugas sin que la herramienta haya ayudado. Esto ocurre por varias razones:

  • Retenciones indirectas: Objetos mantenidos mediante closures, callbacks o elementos internos del framework pueden no mostrar rutas de retención claras.
  • Recursos no cerrados: StreamSubscriptions, AnimationControllers o ScrollControllers olvidados pueden fugarse sin dejar rastros evidentes.
  • Referencias estáticas/globales: Objetos almacenados en variables estáticas o cachés globales no aparecerán como fugas (se retienen intencionalmente, pero podrían ser un error en tu lógica).
  • Plugins de terceros: Las fugas en complementos o bibliotecas externas podrían no ser visibles en el seguimiento de instancias de tu app.
  • Grafos de objetos complejos: Si un objeto se retiene a través de múltiples capas (como una mezcla de widgets, providers y streams), Dart DevTools podría no destacar la causa raíz.

Recuerda: si las técnicas anteriores no ayudaron, tu fuga podría ser sutil. Es hora de ponerte el sombrero de detective y probar la siguiente herramienta de mi cinturón de utilidades: el Dart Leak Tracker. Esta herramienta aún está en desarrollo y no se ha lanzado oficialmente, pero te mostraré cómo usarla en el

próximo artículo

de la serie

(próximamente)

.