Retain Cycle (циклична референца) — ситуација у ARC-у када два или више објеката упућују један на другог преко strong референци, формирајући затворени циклус. Према Apple Memory Management Guide, 2026, retain cycle блокира ослобађање свих објеката у циклусу, јер сваки има retain count ≥ 1. За разлику од цурења меморије у GC-у, retain cycle гарантовано држи објекте живима док је жив бар један спољни учесник циклуса — и чак након губитка свих спољних референци, ако је циклус изолован.
Главне ствари
Retain Cycle — је ситуација у којој два или више објеката поседују један другог преко strong референци, стварајући затворени граф зависности. ARC не може ослободити ниједан од ових објеката, јер је retain count сваког увек ≥ 1: објекат А држи Б, Б држи А, и њихови бројачи се никада не поништавају.
Проблем настаје искључиво у системима са бројањем референци (ARC, MRR). У Garbage Collection-у, сакупљач одређује недостижност на основу графа референци из коренског скупа (root set) — циклуси нису препрека. У ARC-у међутим, циклус је еквивалентан цурењу, јер детерминистичко ослобађање на основу бројача не може решити кружну зависност.
Према WWDC 2012 Session 406, retain cycle је најчешћи узрок цурења меморије у Objective-C и Swift апликацијама. Типични сценарији: parent-child односи са делегатима, затворења која хватају self и слојевите архитектуре са двосмерним везама.
Размотримо класичне сценарије retain cycle-а са којима се сусреће сваки iOS програмер. Разумевање ових образаца је основа за писање безбедног кода са ARC-ом.
Класичан сценарио: родитељски објекат (нпр. UIViewController) креира подређени објекат и постаје његов делегат. Ако оба користе strong референце, настаје retain cycle. Решење — делегат мора бити weak.
// ГРЕШКА: retain cycle кроз 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 преко delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong подразумевано
}
// ИСПРАВКА: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — не задржава
}
У примеру, ParentVC држи strong референцу на ChildVC преко својства child. ChildVC држи strong референцу на ParentVC преко delegate. Циклус је затворен. Исправка: weak var delegate — референца не повећава retain count, и ParentVC може бити ослобођен.
NSTimer — класични извор retain cycle-а. Тајмер држи target (обично self), а target држи тајмер преко својства. Чак и ако је тајмер једнократан, неће се ослободити до invalidate. Решење: увек позивај timer.invalidate() у deinit или viewDidDisappear.
У архитектурама са каскадним власништвом (координатори, рутери) често настају вишестепени циклуси: Coordinator → ViewController → ViewModel → Coordinator (преко callback-а). Свака strong референца у ланцу мора бити свесно одабрана — једна weak референца у било којој карики прекида циклус.
Затворења (closures) у Swift-у хватају спољне променљиве преко strong референце. Ако је затворење ускладиштено као својство објекта (нпр. completion handler) и хвата self, формира се retain cycle: self → closure → self.
Ово је најчешћи извор retain cycle-а у савременом Swift развоју. Настаје нејавно — програмер можда не примети хватање self-а у затворењу, посебно при коришћењу скраћене синтаксе без експлицитног 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()
}
// ✅ Исправка: capture list са weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
Capture list [weak self] ствара слабу референцу на self унутар затворења. Ако DownloadService буде ослобођен пре извршења затворења, self постаје nil и код безбедно излази кроз guard. Ово је стандардни образац за асинхрона затворења у Swift-у — треба га применити увек када је затворење ускладиштено као својство.
unowned self — алтернатива weak self-у, када self гарантовано живи дуже од затворења. Пример: синхроно затворење које се извршава одмах (sorted, filter). У таквим случајевима self је сигурно жив и unowned је безбедан. Међутим, unowned прави crash при приступу ослобођеном објекту — зато се weak сматра безбедним избором подразумевано.
Откривање retain cycle-а у раној фази је кључно за перформансе апликације. Размотримо главне алате и методе идентификације цикличних референци у iOS развоју.
Xcode Memory Debugger (Debug Memory Graph) — визуелни алат који приказује граф објеката у меморији са њиховим референцама. Retain cycle се приказује као затворени ланац strong стрелица. За покретање: кликните на дугме Debug Memory Graph у панелу Debug area током извршавања апликације. Сваки објекат је приказан са типом, адресом и списком референци.
Instruments Leaks — профилер за аутоматско откривање цурења. Снима алокације и анализира граф референци у реалном времену. Открива не само retain cycle, већ и заборављене референце, неослобођене ViewController-е и друга цурења. Leaks указује на тачан објекат и ланац задржавања.
Најједноставнији начин — додај print у deinit сваке кључне класе. Ако deinit није позван при очекиваном уништењу објекта — постоји retain cycle. Овај метод не захтева алате и ефикасан је за почетну дијагностику.
| Алат | Тип | Када применити |
|---|---|---|
| Memory Debugger | Визуелни граф | Ручна провера после навигације |
| Instruments Leaks | Аутоматска анализа | Регресионо тестирање, CI |
| deinit print | Ручно логирање | Развој, code review |
| Malloc Scribble | Runtime-застава | Отклањање грешака use-after-free |
Препоручени приступ: користи deinit логирање у фази развоја, Memory Debugger — при ручном тестирању, Instruments Leaks — у CI/CD pipeline-у за аутоматску регресиону контролу цурења.
Спречавање retain cycle-а је лакше него исправљање у продукцији. Неколико правила која смањују ризик цикличних референци на минимум.
Сви делегати и dataSource морају бити weak. Ово правило је уграђено у UIKit: сви протоколи делегата у Apple SDK-у су декларисани са weak својствима (UITableView.delegate, UICollectionView.dataSource). За сопствене протоколе користи weak var delegate: MyDelegate? и наследи протокол од AnyObject.
Свако затворење које је ускладиштено као својство (completion handler, callback) и хвата self, мора користити [weak self] у capture list-у. Изузетак — затворења која се извршавају одмах и нису ускладиштена (sorted, map, filter). За њих је unowned self безбедан.
У сложеним архитектурама (VIPER, Coordinators, Redux) прати смер strong референци. Власник држи strong референцу на подређеног, али подређени треба да се позива на власника само преко weak или unowned. Једносмерни ток података (unidirectional data flow) поједностављује контролу референци.
// Пример: провера са логирањем deinit
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Коришћење: сви ViewController наслеђују BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// При затварању ProfileVC очекујемо „✅ ProfileVC deallocated” у конзоли
Основна класа са логирањем deinit-а пружа тренутну повратну информацију. Ако се порука не појави при очекиваном затварању екрана — у овој класи постоји retain cycle. Додај ову праксу у шаблон пројекта за све ViewController-е.
Често постављана питања
Retain cycle — специфичан проблем ARC-а, где затворени круг strong референци блокира ослобађање. У GC-у сакупљач анализира достижност из корена (root set), а не бројач референци — зато циклуси нису цурење. У ARC-у међутим, сваки изоловани циклус је гарантовано цурење.
Weak референца не повећава retain count објекта. Ако замените једну од strong референци у циклусу са weak, retain count сваког објекта може да се поништи. Након ослобађања објекта, weak референца се аутоматски поставља на nil, спречавајући приступ мртвој меморији.
Да, retain cycle може укључивати било који број објеката: A → B → C → A. За ослобађање је довољно прекинути једну карику у циклусу — заменити било коју strong референцу са weak или unowned. Алати показују цео граф, а не само парове објеката.
GCD (Grand Central Dispatch) не складишти затворење након извршења. DispatchWorkItem се извршава и ослобађа, чак и ако затворење хвата self. Retain cycle настаје само када је затворење ускладиштено као својство (completion handler у класи), а не када се прослеђује у ред.
Instruments Leaks не проналази увек привремене retain cycle-е (који постоје секунде) и цикличне референце у C/C++ објектима преко bridge-а. За потпуну проверу користи Memory Debugger ручно + deinit логирање свих кључних објеката у сцени.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође