Retain Cycle — esencia, causas y eliminación en el desarrollo de aplicaciones

Autor: IT Sectr Publicado: 2026-03-29 Tiempo de lectura: 8 min

Retain Cycle es una situación en ARC donde dos o más objetos se referencian entre sí a través de referencias fuertes, formando un bucle cerrado. Según la Apple Memory Management Guide, 2026, un retain cycle bloquea la liberación de todos los objetos en el ciclo porque cada uno tiene un retain count ≥ 1. A diferencia de una fuga de memoria en GC, un retain cycle garantiza que los objetos permanezcan vivos mientras al menos un participante externo del ciclo esté vivo — e incluso después de perder todas las referencias externas, si el ciclo está aislado.

Puntos clave

  • Retain Cycle — una cadena cerrada de referencias fuertes que impide que ARC libere los objetos
  • Causa — dos (o más) objetos mantienen referencias fuertes entre sí, imposibilitando que el retain count llegue a cero
  • Consecuencia — fuga de memoria: los objetos permanecen en memoria para siempre, el consumo de RAM aumenta
  • Solución — reemplazar una de las referencias fuertes en el ciclo con weak o unowned
  • Diagnóstico — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

¿Qué es Retain Cycle?

Retain Cycle es una situación en la que dos o más objetos se poseen mutuamente a través de referencias fuertes, creando un grafo de dependencias cerrado. ARC no puede liberar ninguno de estos objetos porque el retain count de cada uno es siempre ≥ 1: el objeto A retiene a B, B retiene a A, y sus contadores nunca llegan a cero.

El problema ocurre exclusivamente en sistemas con conteo de referencias (ARC, MRR). En Garbage Collection, el recolector determina la inalcanzabilidad a través del grafo de referencias desde el conjunto raíz — los ciclos no son un obstáculo. En ARC, sin embargo, un ciclo equivale a una fuga, porque la liberación determinista mediante conteo no puede resolver dependencias circulares.

Según la WWDC 2012 Session 406, retain cycle es la causa más común de fugas de memoria en aplicaciones Objective-C y Swift. Escenarios típicos: relaciones padre-hijo con delegados, closures que capturan self y arquitecturas en capas con relaciones bidireccionales.

Ejemplos de retain cycle en desarrollo iOS

Examinemos escenarios clásicos de retain cycle que todo desarrollador iOS encuentra. Comprender estos patrones es la base para escribir código seguro con ARC.

Parent-Child con delegado

Escenario clásico: un objeto padre (por ejemplo, UIViewController) crea un objeto hijo y se convierte en su delegado. Si ambos usan referencias fuertes, se produce un retain cycle. La solución — el delegado debe ser weak.

swift
// ERROR: retain cycle a través de delegate fuerte
protocol ChildDelegate: AnyObject { }

class ParentVC: UIViewController, ChildDelegate {
    var child: ChildVC?

    func showChild() {
        child = ChildVC()
        child?.delegate = self        // Parent → Child (strong)
    }                                 // Child → Parent (strong mediante delegate)
}                                     // ⚠️ ¡Retain cycle!

class ChildVC: UIViewController {
    var delegate: ChildDelegate?    // ❌ strong por defecto
}

// SOLUCIÓN: delegate weak
class ChildVC: UIViewController {
    weak var delegate: ChildDelegate? // ✅ weak — no retiene
}

En el ejemplo, ParentVC mantiene una referencia fuerte a ChildVC a través de su propiedad child. ChildVC mantiene una referencia fuerte a ParentVC a través de delegate. El ciclo está cerrado. Solución: weak var delegate — la referencia no aumenta el retain count, y ParentVC puede liberarse.

NSTimer y retain cycle

NSTimer es una fuente clásica de retain cycles. El timer retiene su target (normalmente self), y el target retiene el timer a través de una propiedad. Incluso si el timer es de un solo uso, no se liberará hasta que se llame a invalidate. Solución: llamar siempre a timer.invalidate() en deinit o viewDidDisappear.

Arquitecturas en capas

En arquitecturas con propiedad en cascada (coordinadores, enrutadores), suelen producirse ciclos de múltiples pasos: Coordinator → ViewController → ViewModel → Coordinator (a través de un callback). Cada referencia fuerte en la cadena debe elegirse conscientemente — una referencia weak en cualquier eslabón rompe el ciclo.

Retain Cycle en closures de Swift

Las closures en Swift capturan variables externas por referencia fuerte. Si una closure se almacena como propiedad de un objeto (por ejemplo, un completion handler) y captura self, se crea un retain cycle: self → closure → self.

Esta es la fuente más común de retain cycles en el desarrollo moderno con Swift. Ocurre implícitamente — un desarrollador puede no notar la captura de self en una closure, especialmente cuando se usa sintaxis abreviada sin self explícito.

swift
class DownloadService {
    var onComplete: ((Data) -> Void)?
    var result: Data?

    func startDownload() {
        // ❌ Retain cycle: self → onComplete → self
        onComplete = { data in
            self.result = data
            self.notifyUI()
        }

        // ✅ Solución: capture list con weak self
        onComplete = { [weak self] data in
            guard let self else { return }
            self.result = data
            self.notifyUI()
        }
    }

    func notifyUI() { }
}

Una lista de captura [weak self] crea una referencia débil a self dentro de la closure. Si DownloadService se libera antes de que la closure se ejecute, self se vuelve nil, y el código sale de forma segura mediante guard. Este es un patrón estándar para closures asíncronas en Swift — debe usarse siempre que una closure se almacene como propiedad.

Unowned self en closures

unowned self es una alternativa a weak self cuando se garantiza que self vive más que la closure. Ejemplo: closures síncronas que se ejecutan inmediatamente (sorted, filter). En esos casos self definitivamente está vivo, y unowned es seguro. Sin embargo, unowned falla al acceder a un objeto liberado — por lo tanto, weak se considera la opción segura por defecto.

Cómo detectar retain cycle: herramientas de diagnóstico

Detectar retain cycles en una etapa temprana es crítico para el rendimiento de la aplicación. Revisemos las principales herramientas y técnicas para identificar referencias cíclicas en el desarrollo iOS.

Xcode Memory Debugger

Xcode Memory Debugger (Debug Memory Graph) es una herramienta visual que muestra el grafo de objetos en memoria con sus referencias. Un retain cycle aparece como una cadena cerrada de flechas fuertes. Para iniciarlo: haga clic en el botón Debug Memory Graph en el panel Debug area mientras la aplicación se ejecuta. Cada objeto se muestra con su tipo, dirección y lista de referencias.

Instruments Leaks

Instruments Leaks es un profiler para la detección automática de fugas. Registra las asignaciones y analiza el grafo de referencias en tiempo real. Detecta no solo retain cycles, sino también referencias olvidadas, ViewControllers no liberados y otras fugas. Leaks señala el objeto exacto y la cadena de retención.

Registro de deinit

El método más simple es agregar un print en el deinit de cada clase clave. Si deinit no se llama cuando se espera que el objeto se destruya, hay un retain cycle. Este método no requiere herramientas y es efectivo para el diagnóstico inicial.

HerramientaTipoCuándo usarla
Memory DebuggerGrafo visualVerificación manual tras la navegación
Instruments LeaksAnálisis automatizadoPruebas de regresión, CI
deinit printRegistro manualDesarrollo, code review
Malloc ScribbleFlag de runtimeDepuración de use-after-free

Enfoque recomendado: use registro de deinit durante el desarrollo, Memory Debugger durante las pruebas manuales e Instruments Leaks en el pipeline de CI/CD para la detección automatizada de regresiones de fugas.

Prevención de retain cycle y mejores prácticas

Prevenir retain cycles es más fácil que corregirlos en producción. Aquí hay algunas reglas que minimizan el riesgo de referencias cíclicas.

Regla de delegado weak

Todos los delegados y dataSources deben ser weak. Esta regla está incorporada en UIKit: todos los protocolos de delegados en el SDK de Apple se declaran con propiedades weak (UITableView.delegate, UICollectionView.dataSource). Para sus propios protocolos, use weak var delegate: MyDelegate? y herede el protocolo de AnyObject.

Lista de captura en closures

Cualquier closure que se almacene como propiedad (completion handler, callback) y capture self debe usar [weak self] en la lista de captura. La excepción son las closures que se ejecutan inmediatamente y no se almacenan (sorted, map, filter). Para ellas, unowned self es seguro.

Revisión de arquitectura

En arquitecturas complejas (VIPER, Coordinators, Redux), rastree la dirección de las referencias fuertes. El propietario mantiene una referencia fuerte al subordinado, pero el subordinado debe referenciar al propietario solo a través de weak o unowned. El flujo de datos unidireccional simplifica la gestión de referencias.

swift
// Ejemplo: verificación con registro de deinit
class BaseViewController: UIViewController {
    deinit {
        print("✅ \(type(of: self)) deallocated")
    }
}

// Uso: todos los ViewController heredan de BaseViewController
class ProfileVC: BaseViewController {
    var viewModel: ProfileViewModel?
    var onLogout: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        onLogout = { [weak self] in
            self?.dismiss(animated: true)
        }
    }
}
// Al cerrar ProfileVC esperamos "✅ ProfileVC deallocated" en la consola

Una clase base con registro de deinit proporciona retroalimentación instantánea. Si el mensaje no aparece cuando se espera que la pantalla se cierre, hay un retain cycle en esta clase. Agregue esta práctica a la plantilla del proyecto para todos los ViewControllers.

Preguntas frecuentes

¿En qué se diferencia retain cycle de una fuga de memoria en GC?

Retain cycle es un problema específico de ARC donde un bucle cerrado de referencias fuertes bloquea la liberación. En GC, el recolector analiza la accesibilidad desde el conjunto raíz, no los contadores de referencia — por lo tanto, los ciclos no son fugas. En ARC, sin embargo, cualquier ciclo aislado es una fuga garantizada.

¿Cómo rompe una referencia weak un retain cycle?

Una referencia weak no aumenta el retain count de un objeto. Si reemplaza una de las referencias fuertes en un ciclo con weak, el retain count de cada objeto puede llegar a cero. Después de que el objeto se libera, la referencia weak se establece automáticamente a nil, evitando el acceso a memoria liberada.

¿Puede un retain cycle constar de tres o más objetos?

Sí, un retain cycle puede incluir cualquier número de objetos: A → B → C → A. Para romperlo, solo necesita romper un eslabón del ciclo — reemplace cualquier referencia fuerte con weak o unowned. Las herramientas muestran el grafo completo, no solo pares de objetos.

¿Por qué GCD DispatchWorkItem no crea un retain cycle?

GCD (Grand Central Dispatch) no almacena la closure después de su ejecución. El DispatchWorkItem se ejecuta y se libera, incluso si la closure captura self. Un retain cycle solo ocurre cuando una closure se almacena como propiedad (completion handler en una clase), no cuando se pasa a una cola.

¿Qué tipos de retain cycles no detecta Instruments?

Instruments Leaks no siempre encuentra retain cycles temporales (que duran segundos) ni referencias cíclicas en objetos C/C++ a través de bridging. Para una verificación completa, use Memory Debugger manualmente junto con el registro de deinit de todos los objetos clave en la escena.

Resumen

  • Retain Cycle — una cadena cerrada de referencias fuertes que bloquea la liberación de objetos en ARC
  • Causas — delegados con referencia fuerte, closures que capturan self, relaciones padre-hijo bidireccionales
  • Solución — reemplazar una referencia fuerte con weak o unowned rompe el ciclo
  • Closures — los completion handlers almacenados siempre deben usar [weak self]
  • Delegados — siempre weak; el protocolo de delegado debe heredar de AnyObject
  • Detección — Xcode Memory Debugger, Instruments Leaks, registro de deinit
  • Prevención — flujo de datos unidireccional, delegados weak, listas de captura, clase base con deinit

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también