Retain Cycle — essens, orsaker och eliminering i apputveckling

Författare: IT Sectr Publicerad: 2026-03-29 Lästid: 8 min

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 — sluten kedja av strong-referenser där objekt inte kan frigöras av ARC
  • Orsak — två (eller fler) objekt håller en strong-referens till varandra, nollställning av retain count är omöjlig
  • Konsekvenser — minnesläcka: objekt stannar i minnet för alltid, RAM-förbrukningen ökar
  • Lösning — ersätt en av strong-referenserna i cykeln med weak eller unowned
  • Diagnostik — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Vad är Retain Cycle?

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.

Exempel på retain cycle i iOS-utveckling

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.

Parent-Child med delegat

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.

swift
// 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 och retain cycle

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.

Skiktade arkitekturer

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.

Retain Cycle i Swift-slutningar

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.

swift
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 i slutningar

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.

Hur man upptäcker retain cycle: diagnostiska verktyg

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

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

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.

Loggning av deinit

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.

VerktygTypNär ska det användas
Memory DebuggerVisuell grafManuell kontroll efter navigering
Instruments LeaksAutomatisk analysRegressionstestning, CI
deinit printManuell loggningUtveckling, code review
Malloc ScribbleRuntime-flaggaFelsö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.

Förebygga retain cycle och best practices

Att förebygga retain cycle är enklare än att åtgärda det i produktion. Några regler som minimerar risken för cykliska referenser.

Regeln om weak delegate

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.

Capture list i slutningar

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.

Kontroll av arkitektur

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.

swift
// 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

Hur skiljer sig retain cycle från en minnesläcka i GC?

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.

Hur bryter en weak-referens en retain cycle?

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.

Kan en retain cycle bestå av tre eller fler objekt?

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.

Varför skapar GCD DispatchWorkItem inte en retain cycle?

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ö.

Vilka typer av retain cycle upptäcks inte av Instruments?

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

  • Retain Cycle — sluten kedja av strong-referenser som blockerar objektfrigöring i ARC
  • Orsaker — delegater med strong-referens, slutningar som fångar self, tvåvägs parent-child-relationer
  • Lösning — ersätt en strong-referens med weak eller unowned för att bryta cykeln
  • Slutningar — lagrade completion handlers bör alltid använda [weak self]
  • Delegater — alltid weak; delegatprotokollet bör ärva från AnyObject
  • Diagnostik — Xcode Memory Debugger, Instruments Leaks, deinit-loggning
  • Förebyggande — enkelriktat dataflöde, weak delegate, capture list, basklass med deinit

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.

Diskutera projektet

Läs också