Retain Cycle (cyklisk referens) — en situation i ARC där två eller fler objekt refererar till varandra via strong-referenser och bildar en sluten cykel. Enligt Apple Memory Management Guide, 2026 blockerar retain cycle frigörandet av alla objekt i cykeln, eftersom var och en har retain count ≥ 1. Till skillnad från en minnesläcka i GC håller retain cycle garanterat objekten vid liv så länge minst en extern deltagare i cykeln lever — och även efter förlusten av alla externa referenser, om cykeln är isolerad.
Huvudpunkter
Retain Cycle — är en situation där två eller fler objekt äger varandra via strong-referenser och skapar en sluten beroendegraf. ARC kan inte frigöra något av dessa objekt eftersom retain count för varje alltid är ≥ 1: objekt A håller B, B håller A, och deras räknare nollställs aldrig.
Problemet uppstår uteslutande i system med referensräkning (ARC, MRR). I Garbage Collection bestämmer samlaren oåtkomlighet baserat på referensgrafen från rotset (root set) — cykler är inget hinder. I ARC däremot är en cykel likvärdig med en läcka, eftersom deterministisk frigöring baserad på räknare inte kan lösa cirkulära beroenden.
Enligt WWDC 2012 Session 406 är retain cycle den vanligaste orsaken till minnesläckor i Objective-C- och Swift-applikationer. Typiska scenarier: parent-child-relationer med delegater, slutningar som fångar self och skiktade arkitekturer med tvåvägsförbindelser.
Låt oss titta på klassiska retain cycle-scenarier som varje iOS-utvecklare stöter på. Att förstå dessa mönster är grunden för att skriva säker kod med ARC.
Klassiskt scenario: föräldraobjektet (t.ex. UIViewController) skapar ett underordnat objekt och blir dess delegat. Om båda använder strong-referenser uppstår en retain cycle. Lösning — delegaten måste vara weak.
// FEL: retain cycle via 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 via delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong som standard
}
// KORRIGERING: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — håller inte kvar
}
I exemplet håller ParentVC en strong-referens till ChildVC via egenskapen child. ChildVC håller en strong-referens till ParentVC via delegate. Cykeln är sluten. Korrigering: weak var delegate — referensen ökar inte retain count, och ParentVC kan frigöras.
NSTimer — en klassisk källa till retain cycle. Timern håller target (vanligtvis self), och target håller timern via en egenskap. Även om timern är engångs frigörs den inte förrän invalidate. Lösning: anropa alltid timer.invalidate() i deinit eller viewDidDisappear.
I arkitekturer med kaskadägande (koordinatorer, routrar) uppstår ofta flerstegscykler: Coordinator → ViewController → ViewModel → Coordinator (via callback). Varje strong-referens i kedjan måste väljas medvetet — en weak-referens i vilken länk som helst bryter cykeln.
Slutningar (closures) i Swift fångar externa variabler via strong-referens. Om en slutning lagras som en egenskap hos ett objekt (t.ex. completion handler) och fångar self, bildas en retain cycle: self → closure → self.
Detta är den vanligaste källan till retain cycle i modern Swift-utveckling. Den uppstår implicit — utvecklaren kanske inte märker infångningen av self i slutningen, särskilt vid användning av förkortad syntax utan explicit 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()
}
// ✅ Korrigering: capture list med weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
Capture list [weak self] skapar en svag referens till self inuti slutningen. Om DownloadService frigörs innan slutningen körs blir self nil och koden lämnar säkert via guard. Detta är det standardmönster för asynkrona slutningar i Swift — det bör alltid tillämpas när slutningen lagras som en egenskap.
unowned self — ett alternativ till weak self, när self garanterat lever längre än slutningen. Exempel: en synkron slutning som körs omedelbart (sorted, filter). I sådana fall lever self säkert och unowned är säkert. Men unowned kraschar vid åtkomst till ett frigjort objekt — därför anses weak vara det säkra valet som standard.
Tidig upptäckt av retain cycle är avgörande för applikationens prestanda. Låt oss titta på de viktigaste verktygen och metoderna för att identifiera cykliska referenser i iOS-utveckling.
Xcode Memory Debugger (Debug Memory Graph) — ett visuellt verktyg som visar grafen över objekt i minnet med deras referenser. En retain cycle visas som en sluten kedja av strong-pilar. För att starta: klicka på knappen Debug Memory Graph i panelen Debug area medan applikationen körs. Varje objekt visas med typ, adress och referenslista.
Instruments Leaks — en profilerare för automatisk upptäckt av läckor. Registrerar allokeringar och analyserar referensgrafen i realtid. Upptäcker inte bara retain cycle, utan även glömda referenser, frigjorda ViewControllers och andra läckor. Leaks anger det exakta objektet och retentionskedjan.
Det enklaste sättet — lägg till print i deinit för varje nyckelklass. Om deinit inte anropas vid förväntad förstöring av objektet — finns en retain cycle. Denna metod kräver inga verktyg och är effektiv för initial diagnostik.
| Verktyg | Typ | När ska det användas |
|---|---|---|
| Memory Debugger | Visuell graf | Manuell kontroll efter navigering |
| Instruments Leaks | Automatisk analys | Regressionstestning, CI |
| deinit print | Manuell loggning | Utveckling, code review |
| Malloc Scribble | Runtime-flagga | Felsökning av use-after-free |
Rekommenderad metod: använd deinit-loggning i utvecklingsfasen, Memory Debugger — vid manuell testning, Instruments Leaks — i CI/CD-pipelinen för automatisk regressionskontroll av läckor.
Att förebygga retain cycle är enklare än att åtgärda det i produktion. Några regler som minimerar risken för cykliska referenser.
Alla delegater och dataSources måste vara weak. Denna regel är inbyggd i UIKit: alla delegatprotokoll i Apple SDK är deklarerade med weak-egenskaper (UITableView.delegate, UICollectionView.dataSource). För egna protokoll, använd weak var delegate: MyDelegate? och låt protokollet ärva från AnyObject.
Varje slutning som lagras som en egenskap (completion handler, callback) och fångar self måste använda [weak self] i capture list. Undantag — slutningar som körs omedelbart och inte lagras (sorted, map, filter). För dem är unowned self säkert.
I komplexa arkitekturer (VIPER, Coordinators, Redux) spåra riktningen på strong-referenser. Ägaren håller en strong-referens till den underordnade, men den underordnade bör referera till ägaren endast via weak eller unowned. Enkelriktat dataflöde (unidirectional data flow) förenklar referenskontrollen.
// Exempel: kontroll med deinit-loggning
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Användning: alla ViewControllers ärver BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// Vid stängning av ProfileVC förväntas „✅ ProfileVC deallocated” i konsolen
En basklass med deinit-loggning ger omedelbar feedback. Om meddelandet inte visas vid förväntad skärmstängning — finns det en retain cycle i denna klass. Lägg till denna praxis i projektmallen för alla ViewControllers.
Vanliga frågor
Retain cycle — ett specifikt ARC-problem där en sluten cirkel av strong-referenser blockerar frigöring. I GC analyserar samlaren tillgänglighet från roten (root set), inte referensräknaren — därför är cykler inte läckor. I ARC däremot är varje isolerad cykel en garanterad läcka.
En Weak-referens ökar inte objektets retain count. Om du ersätter en av strong-referenserna i cykeln med weak, kan retain count för varje objekt nollställas. Efter att objektet frigörs sätts weak-referensen automatiskt till nil, vilket förhindrar åtkomst till dött minne.
Ja, en retain cycle kan omfatta valfritt antal objekt: A → B → C → A. För att frigöra räcker det att bryta en länk i cykeln — ersätt valfri strong-referens med weak eller unowned. Verktygen visar hela grafen, inte bara objektpar.
GCD (Grand Central Dispatch) lagrar inte slutningen efter exekvering. DispatchWorkItem körs och frigörs, även om slutningen fångar self. En retain cycle uppstår bara när slutningen lagras som en egenskap (completion handler i en klass), inte när den skickas till en kö.
Instruments Leaks hittar inte alltid tillfälliga retain cycles (som finns i sekunder) och cykliska referenser i C/C++-objekt via bridge. För en fullständig kontroll, använd manuellt Memory Debugger + deinit-loggning av alla nyckelobjekt i scenen.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också