Dominando Instruments (Parte 2.5): malloc, free y ARC — cómo funciona la memoria bajo el capó
Entiende visceralmente qué pasa cuando tu código se ejecuta. Visualiza malloc, free, reference counting y retain cycles con componentes interactivos.
En la Parte 2 hablamos del Stack y del Heap. Dijimos que malloc reserva memoria, que free la libera, que ARC lleva la cuenta de las referencias. Pero seamos honestos — si nunca has trabajado a ese nivel, esas palabras se sienten abstractas. Como leer un manual de un carro en otro idioma.
Yo llevo años escribiendo Swift y durante mucho tiempo esos conceptos me pasaban de largo. Sabía que existían, pero no los sentía. No entendía visceralmente qué pasa cuando escribo let person = Person(name: "Juan"). Y esa falta de entendimiento te alcanza eventualmente — cuando tu app tiene un memory leak que no logras rastrear, o cuando Instruments te muestra datos que no puedes interpretar.
Este artículo existe para cerrar esa brecha. No vamos a quedarnos en la teoría — vamos a ver cómo funciona la memoria con visualizaciones interactivas. Al terminar, vas a entender malloc, free, refCount y retain cycles como si los vieras en cámara lenta.
La diferencia entre saber que ARC existe y entender cómo funciona es la diferencia entre manejar un auto y saber qué hacer cuando se descompone.
El Stack: rápido porque es simple
Empecemos por lo fácil. El Stack es la memoria más rápida que tu app tiene porque funciona con una regla brutal: último en entrar, primero en salir. Cuando llamas a una función, se crea un “frame” con espacio para sus variables. Cuando la función termina, el frame se destruye. No hay negociación, no hay búsqueda — solo un puntero que sube y baja.
Vamos a verlo en acción. En el siguiente visualizador, haz clic en cada paso para ver cómo el Stack crece cuando llamamos a una función y se encoge cuando esa función retorna:
¿Cómo se mueve el Stack?
Haz clic en cada paso para ver cómo el stack frame crece y se destruye.
¿Viste lo que pasó? Todo el ciclo de vida de la función — crear variables, calcular, retornar — fue simplemente mover un puntero. No hubo malloc. No hubo free. No hubo ARC. Solo un puntero que avanzó y retrocedió.
Eso es lo que hace al Stack tan rápido. Pero tiene una limitación enorme: solo funciona con datos cuyo tamaño se conoce en tiempo de compilación y que no necesitan sobrevivir a la función que los creó. Para todo lo demás, necesitamos el Heap.
malloc y free: pidiendo y devolviendo habitaciones en un hotel
Si el Stack es una pila de platos — ordenada, predecible — el Heap es un hotel. Tienes habitaciones disponibles de diferentes tamaños, y necesitas un sistema para gestionarlas.
-
malloc(size)es como llegar a la recepción y pedir una habitación. El sistema busca un espacio libre del tamaño que necesitas, lo marca como “ocupado”, te da la llave (un puntero), y te registra en el sistema. Eso cuesta tiempo — no es solo mover un puntero como en el Stack. -
free(pointer)es hacer checkout. Le devuelves la llave al hotel, el sistema marca la habitación como “disponible” otra vez. Pero aquí viene el problema: si nadie hace checkout, la habitación queda ocupada para siempre. Eso es un memory leak.
Y hay otro problema más sutil: la fragmentación. Si reservas y liberas habitaciones en orden aleatorio, terminas con huecos pequeños entre las ocupadas. Puede que tengas 100 habitaciones libres en total, pero ningún bloque contiguo de 50 — y eso hace que una reserva de 50 falle aunque haya espacio “suficiente”.
Juega con el siguiente visualizador. Reserva bloques de diferentes tamaños, libera algunos, y observa cómo el Heap se fragmenta:
malloc y free: el hotel de la memoria
Reserva y libera bloques de memoria. Observa cómo se fragmenta el Heap.
En Swift, tú nunca llamas a malloc o free directamente. El compilador y ARC lo hacen por ti. Pero cada vez que escribes let person = Person(name: "Juan") con una class, eso es un malloc bajo el capó. Y cada vez que esa variable sale de scope y nadie más la referencia, eso es un free.
ARC: el contador invisible que decide quién vive y quién muere
Ahora viene la pregunta clave: ¿quién decide cuándo llamar a free? En lenguajes como C, tú lo haces manualmente — y es una fuente infinita de bugs. En Swift, esa responsabilidad la tiene ARC (Automatic Reference Counting).
El concepto es elegante:
- Cada objeto en el Heap tiene un contador invisible llamado refCount (reference count)
- Cuando creas una referencia al objeto (
let ref = person), el refCount sube: +1 - Cuando esa referencia desaparece (la variable sale de scope, o la pones a
nil), el refCount baja: -1 - Cuando el refCount llega a 0 — nadie más apunta al objeto — ARC llama a
deinity luego afree
Es como un sistema de conteo en el hotel: mientras al menos un huésped tenga llave de la habitación, la habitación sigue asignada. Cuando el último huésped devuelve su llave, checkout automático.
Explora el siguiente simulador para ver ARC en acción. Tiene tres escenarios: el ciclo normal de ARC, un retain cycle (el bug más común de memoria en Swift), y la solución con weak:
ARC: el contador invisible
Explora cómo ARC gestiona la memoria, y qué pasa cuando se forma un retain cycle.
Retain cycles: cuándo pasan y cómo evitarlos
Los retain cycles no son un caso raro. Aparecen en patrones extremadamente comunes:
El delegate clásico
class ViewController { var delegate: SomeDelegate? // strong reference}
class SomeDelegate { var viewController: ViewController? // strong reference back = cycle!}La solución: El delegate siempre debe ser weak:
class SomeDelegate { weak var viewController: ViewController? // weak = no cycle}Closures que capturan self
class DataLoader { var onComplete: (() -> Void)?
func load() { onComplete = { self.updateUI() // self captured strongly = potential cycle! } }}La solución: Usa [weak self] en la capture list:
onComplete = { [weak self] in self?.updateUI() // weak capture = no cycle}- weak → La referencia se vuelve
nilautomáticamente cuando el objeto se libera. Siempre seguro. Úsalo cuando no estás 100% seguro de que el objeto vivirá más que la referencia. - unowned → La referencia NO se vuelve
nil. Si el objeto se libera y accedes a la referencia, tu app crashea. Úsalo solo cuando estás absolutamente seguro de que el objeto vivirá más que la referencia (ej. un child que siempre muere antes que su parent). - Regla práctica: Ante la duda, usa
weak. El costo de unwrappear un optional es infinitamente menor que un crash en producción.
Conectando con Instruments
Todo lo que viste en este artículo — malloc reservando bloques, free liberándolos, refCount subiendo y bajando, retain cycles atrapando memoria — es exactamente lo que Instruments registra y te muestra visualmente. Ahora cuando abras un trace y veas “Allocations” o “Leaks”, vas a saber qué representan esos datos.
Pero antes de sumergirnos en los instrumentos de memoria, necesitamos hablar de metodología. En la Parte 3 vamos a aprender a diagnosticar problemas de rendimiento como un proceso científico — con hipótesis, experimentos y datos. Exploraremos las diferencias entre Time Profiler, CPU Profiler y Processor Trace, y cuándo usar cada uno. También veremos técnicas avanzadas como Charge, Prune y Flatten para manipular los árboles de llamadas con precisión quirúrgica.
Ahora entiendes qué pasa bajo el capó. En la Parte 3 vas a aprender a usar ese conocimiento como un proceso científico — no intuición, sino datos.
Referencias
- Automatic Reference Counting — The Swift Programming Language — La referencia oficial de Apple sobre ARC, strong/weak/unowned references y retain cycles.
- Improving App Responsiveness — Apple Documentation — Guía de Apple sobre cómo el trabajo en el Heap afecta la responsividad de tu app.
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #15: Tipos opacos y tipos de protocolo encajonados
La tercera puerta para salir de 'no quiero nombrar el tipo concreto'. some oculta un tipo concreto conservando su identidad — sin caja, en tiempo de compilación. any borra el tipo en una caja de runtime. Así devuelves por fin un protocolo que tiene un associatedtype.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #14: Genéricos
Un solo swapTwoValues, un solo Stack<Element>, un solo findIndex — escritos una vez, funcionando para todo tipo. Los genéricos dejan que el llamador elija el tipo concreto, que el compilador especialice el código, y que la caja del #13 desaparezca. Array, Dictionary, Optional y Result fueron genéricos todo el tiempo.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #13: Protocolos
Un protocolo es un contrato, no una clase. Dice qué debe hacer un tipo sin decir qué es — y esa sola idea impulsa la delegación, el Equatable sintetizado, los existenciales y las extensiones de protocolo que hacen idiomático a Swift.