Retain Cycle — essenza, cause ed eliminazione nello sviluppo di app

Autore: IT Sectr Pubblicato: 2026-03-29 Tempo di lettura: 8 min

Un Retain Cycle è una situazione in ARC in cui due o più oggetti si referenziano a vicenda tramite riferimenti forti, formando un ciclo chiuso. Secondo la Apple Memory Management Guide, 2026, un retain cycle blocca il rilascio di tutti gli oggetti nel ciclo perché ognuno ha un retain count ≥ 1. A differenza di una perdita di memoria in GC, un retain cycle garantisce che gli oggetti rimangano vivi finché almeno un partecipante esterno del ciclo è vivo — e anche dopo aver perso tutti i riferimenti esterni, se il ciclo è isolato.

Punti chiave

  • Retain Cycle — una catena chiusa di riferimenti forti che impedisce ad ARC di rilasciare gli oggetti
  • Causa — due (o più) oggetti mantengono riferimenti forti l’uno verso l’altro, impossibilità di azzerare il retain count
  • Conseguenza — perdita di memoria: gli oggetti rimangono in memoria per sempre, il consumo di RAM aumenta
  • Soluzione — sostituire uno dei riferimenti forti nel ciclo con weak o unowned
  • Diagnosi — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Cos’è Retain Cycle?

Retain Cycle è una situazione in cui due o più oggetti si possiedono a vicenda tramite riferimenti forti, creando un grafo di dipendenze chiuso. ARC non può rilasciare nessuno di questi oggetti perché il retain count di ciascuno è sempre ≥ 1: l’oggetto A trattiene B, B trattiene A, e i loro contatori non arrivano mai a zero.

Il problema si verifica esclusivamente nei sistemi a conteggio di riferimenti (ARC, MRR). Nel Garbage Collection, il collettore determina l’irraggiungibilità attraverso il grafo dei riferimenti dal set radice — i cicli non sono un ostacolo. In ARC, invece, un ciclo equivale a una perdita, perché il rilascio deterministico tramite conteggio non può risolvere le dipendenze circolari.

Secondo la WWDC 2012 Session 406, il retain cycle è la causa più comune di perdite di memoria nelle applicazioni Objective-C e Swift. Scenari tipici: relazioni genitore-figlio con delegati, closure che catturano self e architetture a strati con relazioni bidirezionali.

Esempi di retain cycle nello sviluppo iOS

Esaminiamo gli scenari classici di retain cycle che ogni sviluppatore iOS incontra. Comprendere questi modelli è la base per scrivere codice sicuro con ARC.

Parent-Child con delegato

Scenario classico: un oggetto genitore (ad esempio, UIViewController) crea un oggetto figlio e diventa il suo delegato. Se entrambi usano riferimenti forti, si verifica un retain cycle. La soluzione — il delegato deve essere weak.

swift
// ERRORE: retain cycle tramite delegato forte
protocol ChildDelegate: AnyObject { }

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

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

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

// CORREZIONE: delegato weak
class ChildVC: UIViewController {
    weak var delegate: ChildDelegate? // ✅ weak — non trattiene
}

Nell’esempio, ParentVC mantiene un riferimento forte a ChildVC tramite la proprietà child. ChildVC mantiene un riferimento forte a ParentVC tramite delegate. Il ciclo è chiuso. Correzione: weak var delegate — il riferimento non aumenta il retain count e ParentVC può essere rilasciato.

NSTimer e retain cycle

NSTimer è una fonte classica di retain cycle. Il timer trattiene il suo target (di solito self), e il target trattiene il timer tramite una proprietà. Anche se il timer è monouso, non verrà rilasciato fino a quando non viene chiamato invalidate. Soluzione: chiamare sempre timer.invalidate() in deinit o viewDidDisappear.

Architetture a strati

Nelle architetture con proprietà a cascata (coordinatori, router), si verificano spesso cicli a più fasi: Coordinator → ViewController → ViewModel → Coordinator (tramite callback). Ogni riferimento forte nella catena deve essere scelto consapevolmente — un riferimento weak in qualsiasi anello rompe il ciclo.

Retain Cycle nelle closure Swift

Le closure in Swift catturano le variabili esterne tramite riferimento forte. Se una closure viene memorizzata come proprietà di un oggetto (ad esempio, un completion handler) e cattura self, crea un retain cycle: self → closure → self.

Questa è la fonte più comune di retain cycle nello sviluppo Swift moderno. Avviene implicitamente — uno sviluppatore potrebbe non notare la cattura di self in una closure, specialmente quando utilizza la sintassi abbreviata senza self esplicito.

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()
        }

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

    func notifyUI() { }
}

Una capture list [weak self] crea un riferimento debole a self all’interno della closure. Se DownloadService viene rilasciato prima dell’esecuzione della closure, self diventa nil e il codice termina in modo sicuro tramite guard. Questo è un modello standard per le closure asincrone in Swift — dovrebbe essere utilizzato ogni volta che una closure viene memorizzata come proprietà.

Unowned self nelle closure

unowned self è un’alternativa a weak self quando self è garantito vivere più a lungo della closure. Esempio: closure sincrone che vengono eseguite immediatamente (sorted, filter). In questi casi, self è sicuramente vivo e unowned è sicuro. Tuttavia, unowned causa un crash quando si accede a un oggetto rilasciato — pertanto weak è considerato la scelta sicura predefinita.

Come rilevare un retain cycle: strumenti diagnostici

Rilevare i retain cycle in una fase precoce è criticamente importante per le prestazioni dell’applicazione. Esaminiamo i principali strumenti e tecniche per identificare i riferimenti ciclici nello sviluppo iOS.

Xcode Memory Debugger

Xcode Memory Debugger (Debug Memory Graph) è uno strumento visivo che mostra il grafo degli oggetti in memoria con i loro riferimenti. Un retain cycle appare come una catena chiusa di frecce forti. Per avviarlo: fare clic sul pulsante Debug Memory Graph nel pannello Debug area mentre l’app è in esecuzione. Ogni oggetto viene mostrato con il suo tipo, indirizzo ed elenco di riferimenti.

Instruments Leaks

Instruments Leaks è un profiler per il rilevamento automatico delle perdite. Registra le allocazioni e analizza il grafo dei riferimenti in tempo reale. Rileva non solo retain cycle, ma anche riferimenti dimenticati, ViewController non rilasciati e altre perdite. Leaks indica l’oggetto esatto e la catena di trattenimento.

Logging di deinit

Il metodo più semplice è aggiungere un print nel deinit di ogni classe chiave. Se deinit non viene chiamato quando ci si aspetta che l’oggetto venga distrutto, c’è un retain cycle. Questo metodo non richiede strumenti ed è efficace per la diagnosi iniziale.

StrumentoTipoQuando usarlo
Memory DebuggerGrafico visivoControllo manuale dopo la navigazione
Instruments LeaksAnalisi automaticaTest di regressione, CI
deinit printLogging manualeSviluppo, code review
Malloc ScribbleFlag runtimeDebug di use-after-free

Approccio consigliato: utilizzare il logging di deinit durante lo sviluppo, Memory Debugger durante i test manuali e Instruments Leaks nella pipeline CI/CD per il rilevamento automatico delle regressioni delle perdite.

Prevenzione del retain cycle e best practices

Prevenire i retain cycle è più facile che correggerli in produzione. Ecco alcune regole che minimizzano il rischio di riferimenti ciclici.

Regola del delegato weak

Tutti i delegati e dataSource devono essere weak. Questa regola è integrata in UIKit: tutti i protocolli di delegati nell’SDK Apple sono dichiarati con proprietà weak (UITableView.delegate, UICollectionView.dataSource). Per i tuoi protocolli, usa weak var delegate: MyDelegate? e fai ereditare il protocollo da AnyObject.

Capture list nelle closure

Qualsiasi closure che viene memorizzata come proprietà (completion handler, callback) e cattura self deve usare [weak self] nella capture list. L’eccezione sono le closure che vengono eseguite immediatamente e non vengono memorizzate (sorted, map, filter). Per queste, unowned self è sicuro.

Revisione dell’architettura

Nelle architetture complesse (VIPER, Coordinatori, Redux), traccia la direzione dei riferimenti forti. Il proprietario mantiene un riferimento forte al subordinato, ma il subordinato deve fare riferimento al proprietario solo tramite weak o unowned. Il flusso di dati unidirezionale semplifica la gestione dei riferimenti.

swift
// Esempio: verifica con logging deinit
class BaseViewController: UIViewController {
    deinit {
        print("✅ \(type(of: self)) deallocated")
    }
}

// Utilizzo: tutti i ViewController ereditano BaseViewController
class ProfileVC: BaseViewController {
    var viewModel: ProfileViewModel?
    var onLogout: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        onLogout = { [weak self] in
            self?.dismiss(animated: true)
        }
    }
}
// Alla chiusura di ProfileVC ci aspettiamo "✅ ProfileVC deallocated" nella console

Una classe base con logging di deinit fornisce feedback immediato. Se il messaggio non appare quando la schermata dovrebbe chiudersi, c’è un retain cycle in questa classe. Aggiungi questa pratica al template di progetto per tutti i ViewController.

Domande frequenti

In cosa differisce il retain cycle da una perdita di memoria in GC?

Retain cycle è un problema specifico di ARC dove un ciclo chiuso di riferimenti forti blocca il rilascio. In GC, il collettore analizza l’accessibilità dal set radice, non i contatori di riferimento — quindi i cicli non sono perdite. In ARC, invece, qualsiasi ciclo isolato è una perdita garantita.

Come fa un riferimento weak a rompere un retain cycle?

Un riferimento weak non aumenta il retain count di un oggetto. Se si sostituisce uno dei riferimenti forti in un ciclo con weak, il retain count di ciascun oggetto può arrivare a zero. Dopo il rilascio dell’oggetto, il riferimento weak viene automaticamente impostato a nil, impedendo l’accesso alla memoria rilasciata.

Un retain cycle può essere composto da tre o più oggetti?

Sì, un retain cycle può includere qualsiasi numero di oggetti: A → B → C → A. Per romperlo, è sufficiente spezzare un anello nel ciclo — sostituire qualsiasi riferimento forte con weak o unowned. Gli strumenti mostrano l’intero grafo, non solo coppie di oggetti.

Perché GCD DispatchWorkItem non crea un retain cycle?

GCD (Grand Central Dispatch) non memorizza la closure dopo l’esecuzione. DispatchWorkItem viene eseguito e rilasciato, anche se la closure cattura self. Un retain cycle si verifica solo quando una closure viene memorizzata come proprietà (completion handler in una classe), non quando viene passata a una coda.

Quali tipi di retain cycle non vengono rilevati da Instruments?

Instruments Leaks non trova sempre retain cycle temporanei (che durano secondi) o riferimenti ciclici in oggetti C/C++ tramite bridging. Per una verifica completa, usa Memory Debugger manualmente insieme al logging di deinit di tutti gli oggetti chiave nella scena.

Riepilogo

  • Retain Cycle — una catena chiusa di riferimenti forti che blocca il rilascio di oggetti in ARC
  • Cause — delegati con riferimento forte, closure che catturano self, relazioni genitore-figlio bidirezionali
  • Soluzione — sostituire un riferimento forte con weak o unowned rompe il ciclo
  • Closure — i completion handler memorizzati devono sempre usare [weak self]
  • Delegati — sempre weak; il protocollo delegato deve ereditare da AnyObject
  • Rilevamento — Xcode Memory Debugger, Instruments Leaks, logging di deinit
  • Prevenzione — flusso di dati unidirezionale, delegati weak, capture list, classe base con deinit

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche