Retain Cycle (циклична референция) — ситуация в ARC, когато два или повече обекта се позовават един на друг чрез strong референции, образувайки затворен цикъл. Според Apple Memory Management Guide, 2026, retain cycle блокира освобождаването на всички обекти в цикъла, тъй като всеки има retain count ≥ 1. За разлика от изтичане на памет в GC, retain cycle гарантирано държи обектите живи, докато е жив поне един външен участник в цикъла — и дори след загуба на всички външни референции, ако цикълът е изолиран.
Основни точки
Retain Cycle — е ситуация, при която два или повече обекта притежават един друг чрез strong референции, създавайки затворен граф на зависимости. ARC не може да освободи нито един от тези обекти, защото retain count на всеки е винаги ≥ 1: обект A държи B, B държи A, и техните броячи никога не се нулират.
Проблемът възниква изключително в системи с броене на референции (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 причинява срив при достъп до освободен обект — затова 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също