Retain Cycle (ciklikus hivatkozás) — helyzet az ARC-ben, amikor két vagy több objektum strong referenciákon keresztül hivatkozik egymásra, zárt ciklust alkotva. A Apple Memory Management Guide, 2026 szerint a retain cycle blokkolja a ciklus összes objektumának felszabadítását, mivel mindegyik rendelkezik retain count ≥ 1 értékkel. Ellentétben a GC memóriaszivárgásával, a retain cycle garantáltan életben tartja az objektumokat, amíg a ciklus legalább egy külső résztvevője él — és még az összes külső hivatkozás elvesztése után is, ha a ciklus izolált.
Fő pontok
Retain Cycle — olyan helyzet, amikor két vagy több objektum strong referenciákon keresztül birtokolja egymást, zárt függőségi gráfot létrehozva. Az ARC egyik objektumot sem tudja felszabadítani, mert mindegyik retain count mindig ≥ 1: A objektum tartja B-t, B tartja A-t, és számlálóik soha nem nullázódnak.
A probléma kizárólag referenciaszámláló rendszerekben (ARC, MRR) fordul elő. A Garbage Collection-ben a gyűjtő a gyökérhalmazból (root set) kiinduló referencia gráf alapján határozza meg az elérhetetlenséget — a ciklusok nem jelentenek akadályt. Az ARC-ben azonban egy ciklus szivárgással egyenértékű, mert a determinisztikus felszabadítás a számláló alapján nem tudja feloldani a körkörös függőséget.
A WWDC 2012 Session 406 szerint a retain cycle a leggyakoribb oka a memóriaszivárgásoknak Objective-C és Swift alkalmazásokban. Tipikus forgatókönyvek: parent-child kapcsolatok delegáltakkal, self-et befogó lezárások és réteges architektúrák kétirányú kapcsolatokkal.
Tekintsük át a klasszikus retain cycle forgatókönyveket, amelyekkel minden iOS fejlesztő találkozik. Ezen minták megértése az ARC-vel való biztonságos kód írásának alapja.
Klasszikus forgatókönyv: a szülő objektum (pl. UIViewController) létrehoz egy gyermek objektumot, és annak delegáltjává válik. Ha mindkettő strong referenciát használ, retain cycle jön létre. Megoldás — a delegáltnak weak-nek kell lennie.
// HIBA: retain cycle strong delegate-en keresztül
protocol ChildDelegate: AnyObject { }
class ParentVC: UIViewController, ChildDelegate {
var child: ChildVC?
func showChild() {
child = ChildVC()
child?.delegate = self // Parent → Child (strong)
} // Child → Parent (strong delegate-en keresztül)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong alapértelmezett
}
// JAVÍTÁS: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — nem tartja
}
A példában a ParentVC strong referenciát tart a ChildVC-re a child tulajdonságon keresztül. A ChildVC strong referenciát tart a ParentVC-re a delegate-en keresztül. A ciklus zárt. Javítás: weak var delegate — a referencia nem növeli a retain count-ot, és a ParentVC felszabadítható.
NSTimer — a retain cycle klasszikus forrása. A timer tartja a target-et (általában self), a target pedig a timer-t egy tulajdonságon keresztül. Még ha a timer egyszer használatos is, nem szabadul fel az invalidate-ig. Megoldás: mindig hívd meg a timer.invalidate()-et a deinit-ben vagy viewDidDisappear-ben.
A kaszkádolt tulajdonlású architektúrákban (koordinátorok, routerek) gyakran több lépcsős ciklusok keletkeznek: Coordinator → ViewController → ViewModel → Coordinator (callback-en keresztül). A lánc minden strong referenciáját tudatosan kell kiválasztani — egy weak referencia bármelyik láncszemben megszakítja a ciklust.
A Swift lezárásai (closures) strong referenciával fogják be a külső változókat. Ha egy lezárás egy objektum tulajdonságaként van tárolva (pl. completion handler) és befogja a self-et, retain cycle keletkezik: self → closure → self.
Ez a retain cycle leggyakoribb forrása a modern Swift fejlesztésben. Implicit módon jön létre — a fejlesztő nem feltétlenül veszi észre a self befogását a lezárásban, különösen a rövidített szintaxis használatakor explicit self nélkül.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ Javítás: capture list weak self-tel
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
A capture list [weak self] gyenge referenciát hoz létre a self-re a lezáráson belül. Ha a DownloadService felszabadul a lezárás végrehajtása előtt, a self nil lesz, és a kód biztonságosan kilép a guard-on keresztül. Ez a szabványos minta aszinkron lezárásokhoz Swift-ben — mindig alkalmazni kell, amikor a lezárás tulajdonságként van tárolva.
unowned self — alternatíva a weak self helyett, amikor a self garantáltan tovább él, mint a lezárás. Példa: azonnal végrehajtódó szinkron lezárás (sorted, filter). Ilyen esetekben a self biztosan él, és az unowned biztonságos. Az unowned azonban crash-el, ha felszabadított objektumhoz fér hozzá — ezért a weak biztonságos választásnak számít alapértelmezésben.
A retain cycle korai észlelése kritikus fontosságú az alkalmazás teljesítménye szempontjából. Tekintsük át a fő eszközöket és módszereket a ciklikus hivatkozások azonosítására iOS fejlesztésben.
Xcode Memory Debugger (Debug Memory Graph) — vizuális eszköz, amely a memóriában lévő objektumok gráfját mutatja a referenciáikkal együtt. A retain cycle a strong nyilak zárt láncaként jelenik meg. Indításhoz: kattints a Debug Memory Graph gombra a Debug area panelen az alkalmazás futása közben. Minden objektum megjelenik típussal, címmel és referencialistával.
Instruments Leaks — profiler a szivárgások automatikus észleléséhez. Rögzíti az allokációkat és valós időben elemzi a referencia gráfot. Nemcsak retain cycle-t, hanem elfelejtett referenciákat, fel nem szabadított ViewController-eket és egyéb szivárgásokat is észlel. A Leaks jelzi a pontos objektumot és a megtartási láncot.
A legegyszerűbb módszer — adj print-et a deinit-hez minden kulcsfontosságú osztályban. Ha a deinit nem hívódik meg az objektum várt megsemmisülésekor — retain cycle van. Ez a módszer nem igényel eszközöket, és hatékony a kezdeti diagnosztikához.
| Eszköz | Típus | Mikor alkalmazandó |
|---|---|---|
| Memory Debugger | Vizuális gráf | Kézi ellenőrzés navigáció után |
| Instruments Leaks | Automatikus elemzés | Regressziós tesztelés, CI |
| deinit print | Kézi naplózás | Fejlesztés, code review |
| Malloc Scribble | Runtime zászló | Use-after-free hibakeresés |
Ajánlott megközelítés: használj deinit naplózást a fejlesztési fázisban, Memory Debugger-t — kézi teszteléskor, Instruments Leaks-ot — a CI/CD pipeline-ban az automatikus regressziós szivárgás ellenőrzéshez.
A retain cycle megelőzése könnyebb, mint a javítása éles környezetben. Néhány szabály, amelyek minimalizálják a ciklikus hivatkozások kockázatát.
Minden delegált és dataSource weak kell legyen. Ez a szabály be van építve az UIKit-be: az Apple SDK összes delegált protokollja weak tulajdonságokkal van deklarálva (UITableView.delegate, UICollectionView.dataSource). Saját protokollokhoz használd a weak var delegate: MyDelegate?-t és származtasd a protokollt az AnyObject-ből.
Minden lezárás, amely tulajdonságként van tárolva (completion handler, callback) és befogja a self-et, [weak self]-et kell használnia a capture list-ben. Kivétel — azonnal végrehajtódó és nem tárolt lezárások (sorted, map, filter). Ezeknél az unowned self biztonságos.
Összetett architektúrákban (VIPER, Coordinators, Redux) kövesd a strong referenciák irányát. A tulajdonos strong referenciát tart a beosztottra, de a beosztott csak weak vagy unowned keresztül hivatkozhat a tulajdonosra. Az egyirányú adatfolyam (unidirectional data flow) leegyszerűsíti a referenciák ellenőrzését.
// Példa: ellenőrzés deinit naplózással
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Használat: minden ViewController a BaseViewController-ből származik
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// A ProfileVC bezárásakor „✅ ProfileVC deallocated” üzenetet várunk a konzolban
A deinit naplózással ellátott alaposztály azonnali visszajelzést ad. Ha az üzenet nem jelenik meg a képernyő várt bezárásakor — ebben az osztályban retain cycle van. Add hozzá ezt a gyakorlatot a projekt sablonhoz minden ViewController számára.
Gyakran Ismételt Kérdések
Retain cycle — az ARC specifikus problémája, ahol a strong referenciák zárt köre blokkolja a felszabadítást. GC-ben a gyűjtő a gyökérből (root set) való elérhetőséget elemzi, nem a referenciaszámlálót — ezért a ciklusok nem minősülnek szivárgásnak. Az ARC-ben viszont minden izolált ciklus garantált szivárgás.
A Weak referencia nem növeli az objektum retain count-ját. Ha a ciklusban lévő egyik strong referenciát weak-re cseréled, minden objektum retain count-ja nullázható. Az objektum felszabadítása után a weak referencia automatikusan nil-re állítódik, megakadályozva a holt memóriához való hozzáférést.
Igen, a retain cycle bármennyi objektumot tartalmazhat: A → B → C → A. A felszabadításhoz elég megszakítani egy láncszemet a ciklusban — cserélj bármelyik strong referenciát weak vagy unowned-re. Az eszközök a teljes gráfot mutatják, nem csak objektumpárokat.
A GCD (Grand Central Dispatch) nem tárolja a lezárást a végrehajtás után. A DispatchWorkItem végrehajtódik és felszabadul, még akkor is, ha a lezárás befogja a self-et. Retain cycle csak akkor keletkezik, amikor a lezárás tulajdonságként van tárolva (completion handler egy osztályban), nem pedig amikor egy sorba kerül átadásra.
Az Instruments Leaks nem mindig találja meg az ideiglenes retain cycle-ket (amelyek másodpercekig léteznek) és a C/C++ objektumokban lévő ciklikus hivatkozásokat bridge-en keresztül. Teljes ellenőrzéshez használj kézi Memory Debugger-t + a scene összes kulcsfontosságú objektumának deinit naplózását.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.