Retain Cycle (referință ciclică) — situație în ARC când două sau mai multe obiecte se referă unul la altul prin referințe strong, formând un ciclu închis. Conform Apple Memory Management Guide, 2026, retain cycle blochează eliberarea tuturor obiectelor din ciclu, deoarece fiecare are retain count ≥ 1. Spre deosebire de scurgerea de memorie în GC, retain cycle menține obiectele în viață atâta timp cât cel puțin un participant extern al ciclului este viu — și chiar după pierderea tuturor referințelor externe, dacă ciclul este izolat.
Puncte cheie
Retain Cycle — este o situație în care două sau mai multe obiecte se dețin reciproc prin referințe strong, creând un graf de dependență închis. ARC nu poate elibera niciunul dintre aceste obiecte, deoarece retain count al fiecăruia este întotdeauna ≥ 1: obiectul A îl ține pe B, B îl ține pe A, iar contoarele lor nu se resetează niciodată.
Problema apare exclusiv în sistemele cu numărare a referințelor (ARC, MRR). În Garbage Collection, colectorul determină inaccesibilitatea pe baza grafului de referințe din setul rădăcină (root set) — ciclurile nu reprezintă un obstacol. În ARC însă, un ciclu este echivalent cu o scurgere, deoarece eliberarea deterministă bazată pe contor nu poate rezolva dependența circulară.
Conform WWDC 2012 Session 406, retain cycle este cea mai frecventă cauză a scurgerilor de memorie în aplicațiile Objective-C și Swift. Scenarii tipice: relații parent-child cu delegați, closure-uri care capturează self și arhitecturi stratificate cu conexiuni bidirecționale.
Să analizăm scenariile clasice de retain cycle cu care se confruntă fiecare dezvoltator iOS. Înțelegerea acestor modele este baza pentru scrierea codului sigur cu ARC.
Scenariul clasic: obiectul părinte (de exemplu, UIViewController) creează un obiect copil și devine delegatul acestuia. Dacă ambele folosesc referințe strong, apare un retain cycle. Soluția — delegatul trebuie să fie weak.
// EROARE: retain cycle prin strong delegate
protocol ChildDelegate: AnyObject { }
class ParentVC: UIViewController, ChildDelegate {
var child: ChildVC?
func showChild() {
child = ChildVC()
child?.delegate = self // Parent → Child (strong)
} // Child → Parent (strong prin delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong implicit
}
// REMEDIU: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — nu reține
}
În exemplu, ParentVC menține o referință strong la ChildVC prin proprietatea child. ChildVC menține o referință strong la ParentVC prin delegate. Ciclul este închis. Corectare: weak var delegate — referința nu crește retain count, iar ParentVC poate fi eliberat.
NSTimer — o sursă clasică de retain cycle. Timerul menține target-ul (de obicei self), iar target-ul menține timerul printr-o proprietate. Chiar dacă timerul este de unică folosință, nu se eliberează până la invalidate. Soluție: apelează întotdeauna timer.invalidate() în deinit sau viewDidDisappear.
În arhitecturile cu deținere în cascadă (coordonatori, routere) apar adesea cicluri în mai mulți pași: Coordinator → ViewController → ViewModel → Coordinator (prin callback). Fiecare referință strong din lanț trebuie aleasă conștient — o singură referință weak în orice verigă întrerupe ciclul.
Closure-urile în Swift capturează variabilele externe prin referință strong. Dacă un closure este stocat ca proprietate a unui obiect (de exemplu, completion handler) și capturează self, se formează un retain cycle: self → closure → self.
Aceasta este cea mai frecventă sursă de retain cycle în dezvoltarea modernă Swift. Apare implicit — dezvoltatorul poate să nu observe capturarea self în closure, în special când folosește sintaxa prescurtată fără self explicit.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ Remediere: capture list cu weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
Capture list [weak self] creează o referință slabă la self în interiorul closure-ului. Dacă DownloadService este eliberat înainte de executarea closure-ului, self devine nil, iar codul iese în siguranță prin guard. Acesta este modelul standard pentru closure-uri asincrone în Swift — trebuie aplicat întotdeauna când closure-ul este stocat ca proprietate.
unowned self — alternativă la weak self, atunci când self trăiește garantat mai mult decât closure-ul. Exemplu: closure sincron care se execută imediat (sorted, filter). În astfel de cazuri, self este cu siguranță viu, iar unowned este sigur. Totuși, unowned generează crash la accesarea unui obiect eliberat — de aceea weak este alegerea sigură implicită.
Depistarea timpurie a retain cycle este esențială pentru performanța aplicației. Să analizăm principalele instrumente și metode de identificare a referințelor ciclice în dezvoltarea iOS.
Xcode Memory Debugger (Debug Memory Graph) — instrument vizual care arată graful obiectelor din memorie cu referințele lor. Retain cycle este afișat ca un lanț închis de săgeți strong. Pentru a-l lansa: apasă butonul Debug Memory Graph din panoul Debug area în timpul rulării aplicației. Fiecare obiect este afișat cu tipul, adresa și lista de referințe.
Instruments Leaks — profiler pentru detectarea automată a scurgerilor. Înregistrează alocările și analizează graful de referințe în timp real. Detectează nu doar retain cycle, ci și referințe uitate, ViewController-uri neeliberate și alte scurgeri. Leaks indică obiectul exact și lanțul de reținere.
Cea mai simplă metodă — adaugă print în deinit al fiecărei clase cheie. Dacă deinit nu este apelat la distrugerea așteptată a obiectului — există un retain cycle. Această metodă nu necesită instrumente și este eficientă pentru diagnosticarea inițială.
| Instrument | Tip | Când să se aplice |
|---|---|---|
| Memory Debugger | Graf vizual | Verificare manuală după navigare |
| Instruments Leaks | Analiză automată | Testare regresie, CI |
| deinit print | Logare manuală | Dezvoltare, code review |
| Malloc Scribble | Fanion runtime | Debugging use-after-free |
Abordarea recomandată: folosește logarea deinit în faza de dezvoltare, Memory Debugger — la testarea manuală, Instruments Leaks — în pipeline-ul CI/CD pentru controlul automat al regresiei scurgerilor.
Prevenirea retain cycle este mai ușoară decât remedierea lui în producție. Câteva reguli care reduc riscul referințelor ciclice la minimum.
Toți delegații și dataSource trebuie să fie weak. Această regulă este încorporată în UIKit: toate protocoalele de delegat din Apple SDK sunt declarate cu proprietăți weak (UITableView.delegate, UICollectionView.dataSource). Pentru propriile protocoale, folosește weak var delegate: MyDelegate? și moștenește protocolul de la AnyObject.
Orice closure care este stocat ca proprietate (completion handler, callback) și capturează self trebuie să folosească [weak self] în capture list. Excepție — closure-urile care se execută imediat și nu sunt stocate (sorted, map, filter). Pentru ele, unowned self este sigur.
În arhitecturile complexe (VIPER, Coordinators, Redux) urmărește direcția referințelor strong. Proprietarul menține o referință strong asupra subordonatului, dar subordonatul trebuie să se refere la proprietar doar prin weak sau unowned. Fluxul de date unidirecțional (unidirectional data flow) simplifică controlul referințelor.
// Exemplu: verificare cu logare deinit
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Utilizare: toate ViewController-urile moștenesc BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// La închiderea ProfileVC așteptăm „✅ ProfileVC deallocated” în consolă
Clasa de bază cu logare deinit oferă feedback instantaneu. Dacă mesajul nu apare la închiderea așteptată a ecranului — în această clasă există un retain cycle. Adaugă această practică în șablonul proiectului pentru toate ViewController-urile.
Întrebări frecvente
Retain cycle — o problemă specifică ARC, unde cercul închis de referințe strong blochează eliberarea. În GC, colectorul analizează accesibilitatea din rădăcină (root set), nu contorul de referințe — prin urmare, ciclurile nu reprezintă scurgeri. În ARC însă, orice ciclu izolat este o scurgere garantată.
Weak reference nu mărește retain count al obiectului. Dacă înlocuiești una dintre referințele strong din ciclu cu weak, retain count al fiecărui obiect se poate reseta. După eliberarea obiectului, weak reference se setează automat la nil, prevenind accesul la memoria moartă.
Da, retain cycle poate include oricâte obiecte: A → B → C → A. Pentru eliberare, este suficient să întrerupi o singură verigă din ciclu — înlocuiește orice referință strong cu weak sau unowned. Instrumentele arată întregul graf, nu doar perechi de obiecte.
GCD (Grand Central Dispatch) nu stochează closure-ul după executare. DispatchWorkItem se execută și este eliberat, chiar dacă closure-ul capturează self. Retain cycle apare doar când closure-ul este stocat ca proprietate (completion handler într-o clasă), nu când este transmis într-o coadă.
Instruments Leaks nu găsește întotdeauna retain cycle-urile temporare (care există secunde) și referințele ciclice în obiectele C/C++ prin bridge. Pentru o verificare completă, folosește Memory Debugger manual + logarea deinit a tuturor obiectelor cheie din scenă.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și