Retain Cycle (cyklická reference) — situace v ARC, kdy dva nebo více objektů odkazuje na sebe navzájem prostřednictvím strong referencí, čímž vytvářejí uzavřený cyklus. Podle Apple Memory Management Guide, 2026 blokuje retain cycle uvolnění všech objektů v cyklu, protože každý má retain count ≥ 1. Na rozdíl od úniku paměti v GC drží retain cycle objekty zaručeně naživu, dokud žije alespoň jeden externí účastník cyklu — a dokonce i po ztrátě všech externích referencí, pokud je cyklus izolovaný.
Hlavní body
Retain Cycle — je situace, kdy dva nebo více objektů vlastní jeden druhého prostřednictvím strong referencí, čímž vytvářejí uzavřený graf závislostí. ARC nemůže uvolnit žádný z těchto objektů, protože retain count každého z nich je vždy ≥ 1: objekt A drží B, B drží A a jejich čítače se nikdy nevynulují.
Problém vzniká výhradně v systémech s počítáním referencí (ARC, MRR). V Garbage Collection určuje kolektor nedosažitelnost na základě grafu referencí z kořenové množiny (root set) — cykly nejsou překážkou. V ARC je však cyklus ekvivalentní úniku, protože deterministické uvolňování založené na čítači nemůže vyřešit kruhovou závislost.
Podle WWDC 2012 Session 406 je retain cycle nejčastější příčinou úniků paměti v Objective-C a Swift aplikacích. Typické scénáře: parent-child vztahy s delegáty, uzávěry zachycující self a vrstvené architektury s obousměrnými vazbami.
Podívejme se na klasické scénáře retain cycle, se kterými se setkává každý iOS vývojář. Pochopení těchto vzorů je základem pro psaní bezpečného kódu s ARC.
Klasický scénář: rodičovský objekt (např. UIViewController) vytvoří podřízený objekt a stane se jeho delegátem. Pokud oba používají strong reference, vzniká retain cycle. Řešení — delegát musí být weak.
// CHYBA: retain cycle prostřednictvím 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 prostřednictvím delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong ve výchozím nastavení
}
// OPRAVA: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — nedrží
}
V příkladu ParentVC drží strong referenci na ChildVC prostřednictvím vlastnosti child. ChildVC drží strong referenci na ParentVC prostřednictvím delegate. Cyklus je uzavřen. Oprava: weak var delegate — reference nezvyšuje retain count a ParentVC může být uvolněn.
NSTimer — klasický zdroj retain cycle. Timer drží target (obvykle self) a target drží timer prostřednictvím vlastnosti. I když je timer jednorázový, neuvolní se do invalidate. Řešení: vždy volejte timer.invalidate() v deinit nebo viewDidDisappear.
V architekturách s kaskádovým vlastnictvím (koordinátory, routery) často vznikají vícestupňové cykly: Coordinator → ViewController → ViewModel → Coordinator (prostřednictvím callbacku). Každá strong reference v řetězci musí být vědomě zvolena — jedna weak reference v kterémkoli článku přeruší cyklus.
Uzávěry (closures) ve Swift zachycují externí proměnné pomocí strong reference. Pokud je uzávěra uložena jako vlastnost objektu (např. completion handler) a zachycuje self, vzniká retain cycle: self → closure → self.
Toto je nejčastější zdroj retain cycle v moderním Swift vývoji. Vzniká implicitně — vývojář si nemusí všimnout zachycení self v uzávěře, zejména při použití zkrácené syntaxe bez explicitního self.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ Oprava: capture list s weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
Capture list [weak self] vytváří slabou referenci na self uvnitř uzávěry. Pokud je DownloadService uvolněn před provedením uzávěry, self se stane nil a kód bezpečně vystoupí přes guard. Toto je standardní vzor pro asynchronní uzávěry ve Swift — měl by být vždy aplikován, když je uzávěra uložena jako vlastnost.
unowned self — alternativa k weak self, když self garantovaně žije déle než uzávěra. Příklad: synchronní uzávěra, která se provádí okamžitě (sorted, filter). V takových případech je self určitě živé a unowned je bezpečný. Nicméně unowned crashuje při přístupu k uvolněnému objektu — proto je weak považován za bezpečnou volbu ve výchozím nastavení.
Včasné odhalení retain cycle je klíčové pro výkon aplikace. Podívejme se na hlavní nástroje a metody identifikace cyklických referencí ve vývoji pro iOS.
Xcode Memory Debugger (Debug Memory Graph) — vizuální nástroj zobrazující graf objektů v paměti s jejich referencemi. Retain cycle se zobrazuje jako uzavřený řetězec strong šipek. Pro spuštění: klikněte na tlačítko Debug Memory Graph v panelu Debug area během běhu aplikace. Každý objekt je zobrazen s typem, adresou a seznamem referencí.
Instruments Leaks — profiler pro automatické odhalování úniků. Zaznamenává alokace a analyzuje graf referencí v reálném čase. Odhaluje nejen retain cycle, ale také zapomenuté reference, neuvolněné ViewControllery a další úniky. Leaks ukazuje přesný objekt a řetězec držení.
Nejjednodušší způsob — přidejte print do deinit každé klíčové třídy. Pokud deinit není volán při očekávaném zničení objektu — existuje retain cycle. Tato metoda nevyžaduje nástroje a je účinná pro počáteční diagnostiku.
| Nástroj | Typ | Kdy použít |
|---|---|---|
| Memory Debugger | Vizuální graf | Ruční kontrola po navigaci |
| Instruments Leaks | Automatická analýza | Regresní testování, CI |
| deinit print | Ruční logování | Vývoj, code review |
| Malloc Scribble | Runtime příznak | Ladění use-after-free |
Doporučený přístup: používejte deinit logování ve fázi vývoje, Memory Debugger — při ručním testování, Instruments Leaks — v CI/CD pipeline pro automatickou regresní kontrolu úniků.
Předcházet retain cycle je snazší než jej opravovat v produkci. Několik pravidel, která minimalizují riziko cyklických referencí.
Všichni delegáti a dataSource musí být weak. Toto pravidlo je zabudováno do UIKit: všechny delegátské protokoly v Apple SDK jsou deklarovány s weak vlastnostmi (UITableView.delegate, UICollectionView.dataSource). Pro vlastní protokoly používejte weak var delegate: MyDelegate? a dědění protokolu po AnyObject.
Každá uzávěra, která je uložena jako vlastnost (completion handler, callback) a zachycuje self, musí používat [weak self] v capture list. Výjimka — uzávěry, které se provádějí okamžitě a nejsou ukládány (sorted, map, filter). Pro ně je unowned self bezpečný.
Ve složitých architekturách (VIPER, Coordinators, Redux) sledujte směr strong referencí. Vlastník drží strong referenci na podřízeného, ale podřízený by měl odkazovat na vlastníka pouze prostřednictvím weak nebo unowned. Jednosměrný tok dat (unidirectional data flow) zjednodušuje kontrolu referencí.
// Příklad: kontrola s logováním deinit
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Použití: všechny ViewControllery dědí BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// Při zavření ProfileVC očekáváme v konzoli „✅ ProfileVC deallocated”
Základní třída s logováním deinit poskytuje okamžitou zpětnou vazbu. Pokud se zpráva neobjeví při očekávaném zavření obrazovky — v této třídě je retain cycle. Přidejte tuto praktiku do šablony projektu pro všechny ViewControllery.
Často kladené otázky
Retain cycle — specifický problém ARC, kde uzavřený kruh strong referencí blokuje uvolnění. V GC analyzuje kolektor dosažitelnost z kořene (root set), nikoli čítač referencí — proto cykly nejsou únikem. V ARC je však každý izolovaný cyklus zaručeným únikem.
Weak reference nezvyšuje retain count objektu. Pokud nahradíte jednu ze strong referencí v cyklu za weak, retain count každého objektu se může vynulovat. Po uvolnění objektu se weak reference automaticky nastaví na nil, čímž zabrání přístupu k mrtvé paměti.
Ano, retain cycle může zahrnovat libovolný počet objektů: A → B → C → A. Pro uvolnění stačí přerušit jeden článek v cyklu — nahradit libovolnou strong referenci za weak nebo unowned. Nástroje zobrazují celý graf, nejen páry objektů.
GCD (Grand Central Dispatch) neukládá uzávěru po provedení. DispatchWorkItem se provede a uvolní, i když uzávěra zachycuje self. Retain cycle vzniká pouze tehdy, když je uzávěra uložena jako vlastnost (completion handler ve třídě), nikoli když je předána do fronty.
Instruments Leaks ne vždy najde dočasné retain cycle (existující sekundy) a cyklické reference v C/C++ objektech přes bridge. Pro úplnou kontrolu použijte ručně Memory Debugger + deinit logování všech klíčových objektů ve scéně.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také