AlanJereb.com
Programación

Flutter – Detección de fugas de memoria – Introducción

Mar 15th, 2025

Una mañana temprano a finales de diciembre de 2024, cuando la mayoría de nosotros ya estábamos mentalmente de vacaciones de Año Nuevo, llegué al trabajo un poco más tarde de lo habitual. Cuando llegué a la oficina y estaba colgando mi atuendo de 20 capas en un perchero, un compañero de trabajo se volvió hacia mí y dijo: «¿Has visto el correo electrónico?». Yo, molesto: «¿Qué correo electrónico?», ya que he hecho de mi misión de vida evitar hacer cosas relacionadas con el trabajo en casa. Él responde: «Joe de NOMBRE-DE-LA-EMPRESA-OCULTADO nos envió un mensaje diciendo que todos sus dispositivos están congelados o se han bloqueado».

El intercambio anterior marca el comienzo de mi búsqueda de fugas de memoria y optimización de aplicaciones en Flutter, que duró dos meses.

Ustedes, que probablemente están pasando por lo mismo, podrían encontrar esta serie de publicaciones en el blog como una bendición. En esta serie de 4 partes, describiré todo lo que he aprendido sobre las fugas de memoria, sus causas y cómo solucionarlas. Primero tocaré un poco de teoría sobre lo que causa las fugas de memoria y cómo evitarlas, y luego continuaré con las formas de detectar tus fugas de memoria, desde las técnicas más simples hasta las más avanzadas.

Con la información que recibas, no te tomará dos meses encontrar y resolver tus fugas de memoria.

¿Qué es una fuga de memoria?

Imagina que estás saliendo de una habitación de hotel, y la recepcionista olvida marcar tu habitación como vacía. Aunque esa habitación no esté en uso, nadie más puede usarla.

Imagina que asignas un objeto a un lugar en la memoria y olvidas liberarlo cuando ya no lo necesitas. Aunque ese espacio de memoria podría reutilizarse, nadie más puede usarlo.

Tener un objeto ocupando tu espacio de memoria se llama fuga de memoria.

¿Por qué deberías evitar las fugas de memoria?

En resumen, las fugas de memoria causan un alto uso de memoria, ralentizan el rendimiento, provocan bloqueos y, en dispositivos móviles, consumen más batería.

Recolección de basura de Dart y objetos no recolectables

Centrémonos en Flutter y sus funcionalidades de gestión de memoria.

El lenguaje de programación Dart, la base de Flutter, utiliza un mecanismo de recolección de basura generacional (GC) para gestionar el uso de memoria del sistema.

Utiliza un enfoque de dos niveles que se ocupa de los objetos de vida corta y los objetos de vida larga, respectivamente:

  • Young Space Scavenger (Nursery): Asigna un espacio limitado en la memoria para objetos de vida corta (como datos temporales creados al construir widgets), que se escanea una vez que el espacio limitado se llena. Los objetos no utilizados se liberan (eliminan), mientras que los objetos aún activos se copian en otro espacio de memoria asignado. Este proceso es muy rápido y eficiente.
  • Parallel Mark-Sweep: Después de un tiempo, los objetos se transfieren desde el nursery y se promueven a un nuevo recolector "mark sweep". Este funciona lentamente para detectar objetos no utilizados que han superado su propósito, pero lo hace de manera segura. En comparación con el GC del Nursery, este funciona cuando la aplicación está inactiva (durante los tiempos de inactividad) para garantizar una interacción de usuario fluida y sin bloqueos.
Diagrama de flujo del proceso de recolección de basura en Dart

Diagrama de flujo del proceso de recolección de basura en Dart

Cualquier objeto sin referencias adjuntas es un objetivo para el GC de Dart. Si un objeto todavía tiene referencias que apuntan a él, incluso cuando ya no se usa, el GC de Dart lo omitirá, y el objeto permanecerá en la memoria. Etiquetémoslos como objetos no recolectables.

Entonces, ¿necesitas hacer algo? ¿El recolector de basura de Dart hace todo el trabajo por ti? ¿Dart se encarga de las asignaciones de memoria no utilizadas?

Sí y no.

Es cierto que, a diferencia de

C

, donde debes encargarte de todas tus asignaciones de memoria tú mismo, Dart hace la mayor parte del trabajo pesado por ti. Sin embargo, hay excepciones para las cuales el GC de Dart no es lo suficientemente inteligente como para encargarse de ellas.

Objetos no recolectables

Como Dart es un lenguaje orientado a objetos, significa que todo es un objeto en el fondo, incluso los tipos primitivos como int, double, bool, String y null.

A primera vista, lo harás muy bien si te encargas de todos los objetos Dart con el método dispose en ellos. El desarrollador debe liberar estos objetos "manualmente" cuando el uso previsto del objeto haya terminado.

Pero lo anterior no cubre todo. No todos los objetos no recolectables tienen un método dispose.

Aquí hay una lista de los objetos que más comúnmente generan fugas:

  • variables globales: establécelas en null cuando ya no sean necesarias
  • escuchas/suscripciones de eventos: cancela o elimina las escuchas
  • cierres que capturan el contexto de construcción: evita capturas innecesarias o libera los cierres
  • cachés y mapas: usa referencias débiles o limpia el caché cuando no sea necesario
  • temporizadores y devoluciones de llamada periódicas: cancela los temporizadores cuando ya no sean necesarios
  • recursos nativos: cierra o libera explícitamente los recursos
  • bibliotecas de terceros: revisa la documentación para los métodos de limpieza

Cómo deshacerse de los objetos no recolectables

Si estás interesado en ejemplos y plantillas sobre cómo deshacerte de los objetos no recolectables mencionados anteriormente, dirígete a la

de esta serie.