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 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.
Esaminiamo gli scenari classici di retain cycle che ogni sviluppatore iOS incontra. Comprendere questi modelli è la base per scrivere codice sicuro con ARC.
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.
// 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 è 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.
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.
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.
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 è 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.
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 (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 è 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.
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.
| Strumento | Tipo | Quando usarlo |
|---|---|---|
| Memory Debugger | Grafico visivo | Controllo manuale dopo la navigazione |
| Instruments Leaks | Analisi automatica | Test di regressione, CI |
| deinit print | Logging manuale | Sviluppo, code review |
| Malloc Scribble | Flag runtime | Debug 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.
Prevenire i retain cycle è più facile che correggerli in produzione. Ecco alcune regole che minimizzano il rischio di riferimenti ciclici.
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.
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.
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.
// 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
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.
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.
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.
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.
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
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.
Leggi anche