ARC y los ciclos de retención — recorre el weak-strong dance en un closure
Los ciclos de retención son la trampa clásica de ARC en las entrevistas — el weak-strong dance separa a quien leyó sobre gestión de memoria de quien realmente la depuró.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
ARC y los ciclos de retención — recorre el weak-strong dance en un closure
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#ARC en un párrafo
Automatic Reference Counting es la estrategia de gestión de memoria de Swift (y de Objective-C): el compilador inserta llamadas de retain y release alrededor de cada referencia fuerte a una instancia de class, y la instancia se libera en el instante en que su conteo de referencias llega a cero. Es determinista — a diferencia de un recolector de basura de rastreo, no hay una pausa del mundo para barrer; la liberación ocurre en el instante en que desaparece la última referencia fuerte —, pero ese determinismo tiene un filo peligroso: si dos objetos se sostienen mutuamente con referencias fuertes, ningún conteo de referencias llega jamás a cero, y ninguno de los dos objetos se libera nunca. Eso es un ciclo de retención, y es una fuga de memoria durante toda la vida del proceso de la app.
#Dónde los closures crean ciclos
Los closures capturan las variables a las que hacen referencia, y por defecto las capturan de forma fuerte — incluido self. Eso está bien de forma aislada. El ciclo se forma cuando la relación es mutua: un objeto guarda un closure como propiedad (un completion handler, un callback guardado para después), y ese closure captura self fuertemente. Ahora el objeto sostiene al closure fuertemente, y el closure sostiene al objeto fuertemente — un ciclo de dos nodos que el conteo de referencias de ARC no puede ver ni romper por sí solo, porque desde la perspectiva de ARC ambos objetos genuinamente tienen una referencia fuerte activa apuntándoles.
#El weak-strong dance
La solución es declarar la captura como [weak self] en la lista de captura del closure, lo cual hace que el closure sostenga una referencia débil y opcional a self — una que no lo mantiene con vida y que automáticamente se vuelve nil si self se libera en otro lugar. Pero el cuerpo de un closure suele hacer varias cosas a lo largo de varias líneas, y si se accediera a self como self? en cada línea, en teoría podría liberarse entre dos de esas líneas en una cola concurrente, dejándote con una ejecución parcial inconsistente. El "baile" es la segunda mitad: volver a vincular de inmediato el opcional débil a una constante local fuerte con guard let self else { return } (la forma abreviada desde Swift 5.7+ de guard let self = self else { return }). Durante el resto de esa invocación del closure, self es una referencia fuerte garantizada como viva — pero está limitada a esa única invocación, así que no recrea el ciclo.
#weak vs. unowned
Las referencias weak son siempre opcionales, y Swift las pone automáticamente en nil en el instante en que el objeto referenciado se libera — seguro, al pequeño costo de un optional extra que desenvolver. Las referencias unowned no son opcionales y asumen que el objeto referenciado siempre vivirá más que la referencia; acceder a una referencia unowned después de que su objeto se liberó es comportamiento indefinido — típicamente un crash inmediato, a veces algo peor. unowned(unsafe) va más allá y omite incluso la comprobación de seguridad en tiempo de ejecución para ganar algo de rendimiento, cambiando un crash garantizado por una posible corrupción de memoria si te equivocas. Recurre a unowned solo cuando puedas demostrar que los ciclos de vida están atados entre sí (un padre que garantiza vivir más que un hijo al que le entrega un closure); por defecto usa weak.
#Encontrar ciclos que no querías crear
La herramienta más confiable es el Debug Memory Graph de Xcode (el ícono que parece tres círculos conectados en la barra de depuración): dispara el flujo que debería liberar una pantalla, y luego abre el memory graph — cualquier objeto que siga vivo y debería haber desaparecido aparece ahí, a menudo con un signo de exclamación morado marcando una fuga probable, y el gráfico dibuja la cadena de referencias real que lo mantiene con vida. Las plantillas Leaks y Allocations de Instruments detectan la misma clase de problema en sesiones más largas. Una técnica manual barata para sospechosos: añade un print o un breakpoint en deinit y confirma que se dispare cuando esperas que el objeto desaparezca — el silencio es la fuga.
#No solo closures
Las propiedades delegate son un ciclo de retención clásico sin closures de por medio si se declaran como var delegate: SomeDelegate en lugar de weak var delegate: SomeDelegate? — el objeto que delega y su delegado suelen sostener referencias entre sí. Los grafos de objetos padre-hijo tienen la misma forma: si un hijo guarda una referencia fuerte de vuelta a su padre (para llamarlo de regreso) mientras el padre sostiene una referencia fuerte hacia el hijo, eso es un ciclo sin importar si hay un closure de por medio.
#En código
final class ImageDownloader {var onComplete: ((UIImage?) -> Void)?func download(from url: URL) {URLSession.shared.dataTask(with: url) { data, _, _ inlet image = data.flatMap(UIImage.init)DispatchQueue.main.async {self.onComplete?(image) // capturado fuertemente, self implícito}}.resume()}deinit {print("ImageDownloader liberado")}}final class ProfileViewController: UIViewController {private let downloader = ImageDownloader()override func viewDidLoad() {super.viewDidLoad()// ❌ Ciclo de retención: self -> downloader -> closure onComplete -> selfdownloader.onComplete = { image inself.imageView.image = image}}deinit {print("ProfileViewController liberado") // nunca se imprime — hay una fuga}}
final class ProfileViewController: UIViewController {private let downloader = ImageDownloader()override func viewDidLoad() {super.viewDidLoad()// ✅ El weak-strong dancedownloader.onComplete = { [weak self] image inguard let self else { return }self.imageView.image = image}}deinit {print("ProfileViewController liberado") // ahora se imprime correctamente}}
#El ciclo, visualmente
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Qué hace que un closure cree un ciclo de retención con el objeto que lo posee?
2. ¿Por qué volver a vincular a un self local fuerte justo después de una captura [weak self], en lugar de usar self? en cada línea?
3. ¿Cuál es el riesgo clave de unowned comparado con weak?