Retain Cycle — esența, cauzele apariției și eliminarea în dezvoltarea aplicațiilor

Autor: IT Sectr Publicat: 2026-03-29 Timp de citire: 8 min

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 — lanț închis de referințe strong, prin care obiectele nu pot fi eliberate de ARC
  • Cauză — două (sau mai multe) obiecte păstrează referințe strong unul la altul, resetarea retain count este imposibilă
  • Consecințe — scurgere de memorie: obiectele rămân în memorie permanent, consumul RAM crește
  • Soluție — înlocuirea uneia dintre referințele strong din ciclu cu weak sau unowned
  • Diagnosticare — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Ce este Retain Cycle?

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.

Exemple de retain cycle în dezvoltarea iOS

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.

Parent-Child cu delegat

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.

swift
// 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 și retain cycle

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.

Arhitecturi stratificate

Î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.

Retain Cycle în closure-uri Swift

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.

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

        // ✅ 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 în closure-uri

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ă.

Cum să depistezi retain cycle: instrumente de diagnosticare

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

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

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.

Logarea deinit

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ă.

InstrumentTipCând să se aplice
Memory DebuggerGraf vizualVerificare manuală după navigare
Instruments LeaksAnaliză automatăTestare regresie, CI
deinit printLogare manualăDezvoltare, code review
Malloc ScribbleFanion runtimeDebugging 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 și best practices

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.

Regula weak delegate

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.

Capture list în closure-uri

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.

Verificarea arhitecturii

Î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.

swift
// 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

Cu ce diferă retain cycle de scurgerea de memorie în GC?

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ă.

Cum întrerupe weak reference un retain cycle?

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ă.

Poate un retain cycle să includă trei sau mai multe obiecte?

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.

De ce GCD DispatchWorkItem nu creează retain cycle?

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ă.

Ce tipuri de retain cycle nu sunt detectate de Instruments?

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

  • Retain Cycle — lanț închis de referințe strong care blochează eliberarea obiectelor în ARC
  • Cauze — delegați cu referință strong, closure-uri cu capturarea self, relații bidirecționale parent-child
  • Soluție — înlocuirea unei referințe strong cu weak sau unowned întrerupe ciclul
  • Closure-uri — completion handler-urile stocate trebuie să folosească întotdeauna [weak self]
  • Delegați — întotdeauna weak; protocolul delegatului trebuie să moștenească AnyObject
  • Diagnosticare — Xcode Memory Debugger, Instruments Leaks, logarea deinit
  • Prevenire — flux de date unidirecțional, weak delegate, capture list, clasă de bază cu deinit

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.

Discutați proiectul

Citiți și