Retain Cycle — essentie, oorzaken en oplossing in app-ontwikkeling

Auteur: IT Sectr Gepubliceerd: 2026-03-29 Leestijd: 8 min

Retain Cycle (cyclische verwijzing) — een situatie in ARC waarin twee of meer objecten naar elkaar verwijzen via strong-referenties, waardoor een gesloten cyclus ontstaat. Volgens de Apple Memory Management Guide, 2026 blokkeert een retain cycle het vrijgeven van alle objecten in de cyclus, omdat elk een retain count van ≥ 1 heeft. In tegenstelling tot een geheugenlek in GC, houdt een retain cycle objecten gegarandeerd in leven zolang ten minste één externe deelnemer van de cyclus leeft — en zelfs na het verlies van alle externe verwijzingen, als de cyclus geïsoleerd is.

Belangrijkste punten

  • Retain Cycle — gesloten keten van strong-referenties, waardoor objecten niet door ARC kunnen worden vrijgegeven
  • Oorzaak — twee (of meer) objecten houden een strong-referentie naar elkaar vast, resetten van retain count is onmogelijk
  • Gevolgen — geheugenlek: objecten blijven voor altijd in het geheugen, RAM-verbruik stijgt
  • Oplossing — vervang een van de strong-referenties in de cyclus door weak of unowned
  • Diagnose — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Wat is een Retain Cycle?

Retain Cycle — is een situatie waarin twee of meer objecten elkaar vasthouden via strong-referenties, waardoor een gesloten afhankelijkheidsgraaf ontstaat. ARC kan geen van deze objecten vrijgeven, omdat de retain count van elk altijd ≥ 1 is: object A houdt B vast, B houdt A vast en hun tellers worden nooit gereset.

Het probleem doet zich uitsluitend voor in systemen met referentietelling (ARC, MRR). In Garbage Collection bepaalt de verzamelaar onbereikbaarheid op basis van de referentiegraaf vanuit de wortelverzameling (root set) — cycli vormen geen belemmering. In ARC daarentegen is een cyclus gelijk aan een lek, omdat deterministisch vrijgeven op basis van een teller de circulaire afhankelijkheid niet kan oplossen.

Volgens WWDC 2012 Session 406 is retain cycle de meest voorkomende oorzaak van geheugenlekken in Objective-C- en Swift-applicaties. Typische scenario's: parent-child-relaties met delegates, sluitingen die self vastleggen en gelaagde architecturen met bidirectionele verbindingen.

Voorbeelden van retain cycle in iOS-ontwikkeling

Laten we de klassieke retain cycle-scenario's bekijken waar elke iOS-ontwikkelaar mee te maken krijgt. Inzicht in deze patronen is de basis voor het schrijven van veilige code met ARC.

Parent-Child met delegate

Klassiek scenario: het bovenliggende object (bijv. UIViewController) maakt een onderliggend object aan en wordt de delegate ervan. Als beide strong-referenties gebruiken, ontstaat een retain cycle. Oplossing — de delegate moet weak zijn.

swift
// FOUT: 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 standaard
}

// CORRECTIE: weak delegate
class ChildVC: UIViewController {
    weak var delegate: ChildDelegate? // ✅ weak — houdt niet vast
}

In het voorbeeld houdt ParentVC een strong-referentie naar ChildVC vast via de eigenschap child. ChildVC houdt een strong-referentie naar ParentVC vast via delegate. De cyclus is gesloten. Correctie: weak var delegate — de referentie verhoogt retain count niet, waardoor ParentVC kan worden vrijgegeven.

NSTimer en retain cycle

NSTimer — een klassieke bron van retain cycle. De timer houdt de target (meestal self) vast, en de target houdt de timer vast via een eigenschap. Zelfs als de timer eenmalig is, wordt hij pas vrijgegeven na invalidate. Oplossing: roep altijd timer.invalidate() aan in deinit of viewDidDisappear.

Gelaagde architecturen

In architecturen met trapsgewijs eigendom (coördinatoren, routers) ontstaan vaak meerstapscycli: Coordinator → ViewController → ViewModel → Coordinator (via callback). Elke strong-referentie in de keten moet bewust worden gekozen — één weak-referentie in elke schakel verbreekt de cyclus.

Retain Cycle in Swift-sluitingen

Sluitingen (closures) in Swift leggen externe variabelen vast via strong-referentie. Als een sluiting wordt opgeslagen als eigenschap van een object (bijv. completion handler) en self vastlegt, ontstaat een retain cycle: self → closure → self.

Dit is de meest voorkomende bron van retain cycle in moderne Swift-ontwikkeling. Het ontstaat impliciet — de ontwikkelaar kan het vastleggen van self in de sluiting over het hoofd zien, vooral bij gebruik van verkorte syntax zonder expliciete 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()
        }

        // ✅ Correctie: capture list met weak self
        onComplete = { [weak self] data in
            guard let self else { return }
            self.result = data
            self.notifyUI()
        }
    }

    func notifyUI() { }
}

Capture list [weak self] creëert een zwakke referentie naar self binnen de sluiting. Als DownloadService wordt vrijgegeven voordat de sluiting wordt uitgevoerd, wordt self nil en verlaat de code veilig via guard. Dit is het standaardpatroon voor asynchrone sluitingen in Swift — het moet altijd worden toegepast wanneer de sluiting als eigenschap wordt opgeslagen.

Unowned self in sluitingen

unowned self — een alternatief voor weak self, wanneer self gegarandeerd langer leeft dan de sluiting. Voorbeeld: een synchrone sluiting die onmiddellijk wordt uitgevoerd (sorted, filter). In dergelijke gevallen leeft self zeker en is unowned veilig. Echter, unowned crasht bij toegang tot een vrijgegeven object — daarom wordt weak als de veilige standaardkeuze beschouwd.

Hoe retain cycle detecteren: diagnostische hulpmiddelen

Vroegtijdige detectie van retain cycle is cruciaal voor de prestaties van de applicatie. Laten we de belangrijkste hulpmiddelen en methoden bekijken voor het identificeren van cyclische verwijzingen in iOS-ontwikkeling.

Xcode Memory Debugger

Xcode Memory Debugger (Debug Memory Graph) — een visueel hulpmiddel dat de graaf van objecten in het geheugen toont met hun referenties. Een retain cycle wordt weergegeven als een gesloten keten van strong-pijlen. Om te starten: klik op de knop Debug Memory Graph in het Debug area-paneel tijdens het uitvoeren van de applicatie. Elk object wordt getoond met type, adres en lijst van referenties.

Instruments Leaks

Instruments Leaks — een profiler voor automatische detectie van lekken. Registreert allocaties en analyseert de referentiegraaf in realtime. Detecteert niet alleen retain cycles, maar ook vergeten referenties, niet-vrijgegeven ViewControllers en andere lekken. Leaks geeft het exacte object en de retentieketen aan.

Deinit-logging

De eenvoudigste methode — voeg een print toe in deinit van elke belangrijke klasse. Als deinit niet wordt aangeroepen bij de verwachte vernietiging van het object — is er een retain cycle. Deze methode vereist geen hulpmiddelen en is effectief voor initiële diagnostiek.

HulpmiddelTypeWanneer toepassen
Memory DebuggerVisuele graafHandmatige controle na navigatie
Instruments LeaksAutomatische analyseRegressietesten, CI
deinit printHandmatige loggingOntwikkeling, code review
Malloc ScribbleRuntime-vlagDebuggen van use-after-free

Aanbevolen aanpak: gebruik deinit-logging in de ontwikkelingsfase, Memory Debugger — bij handmatig testen, Instruments Leaks — in de CI/CD-pipeline voor automatische regressiecontrole van lekken.

Preventie van retain cycle en best practices

Voorkomen van retain cycle is eenvoudiger dan herstellen in productie. Enkele regels die het risico op cyclische verwijzingen tot een minimum beperken.

De weak delegate-regel

Alle delegates en dataSources moeten weak zijn. Deze regel is ingebouwd in UIKit: alle delegate-protocollen in de Apple SDK zijn gedeclareerd met weak-eigenschappen (UITableView.delegate, UICollectionView.dataSource). Gebruik voor eigen protocollen weak var delegate: MyDelegate? en laat het protocol overerven van AnyObject.

Capture list in sluitingen

Elke sluiting die als eigenschap wordt opgeslagen (completion handler, callback) en self vastlegt, moet [weak self] gebruiken in de capture list. Uitzondering — sluitingen die onmiddellijk worden uitgevoerd en niet worden opgeslagen (sorted, map, filter). Daarvoor is unowned self veilig.

Architectuur controleren

In complexe architecturen (VIPER, Coordinators, Redux) moet u de richting van strong-referenties volgen. De eigenaar houdt een strong-referentie naar de ondergeschikte vast, maar de ondergeschikte mag alleen via weak of unowned naar de eigenaar verwijzen. Unidirectionele gegevensstroom (unidirectional data flow) vereenvoudigt de controle van referenties.

swift
// Voorbeeld: controle met deinit-logging
class BaseViewController: UIViewController {
    deinit {
        print("✅ \(type(of: self)) deallocated")
    }
}

// Gebruik: alle ViewControllers erven van BaseViewController
class ProfileVC: BaseViewController {
    var viewModel: ProfileViewModel?
    var onLogout: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        onLogout = { [weak self] in
            self?.dismiss(animated: true)
        }
    }
}
// Bij sluiten van ProfileVC verwachten we „✅ ProfileVC deallocated” in de console

Een basisklasse met deinit-logging geeft onmiddellijke feedback. Als het bericht niet verschijnt bij het verwachte sluiten van het scherm — bevat deze klasse een retain cycle. Voeg deze praktijk toe aan het projectsjabloon voor alle ViewControllers.

Veelgestelde vragen

Hoe verschilt retain cycle van een geheugenlek in GC?

Retain cycle — een specifiek ARC-probleem waarbij een gesloten kring van strong-referenties het vrijgeven blokkeert. In GC analyseert de verzamelaar bereikbaarheid vanuit de wortel (root set), niet de referentieteller — daarom zijn cycli geen lek. In ARC daarentegen is elke geïsoleerde cyclus een gegarandeerd lek.

Hoe verbreekt een weak-referentie een retain cycle?

Een weak-referentie verhoogt de retain count van een object niet. Als u een van de strong-referenties in de cyclus vervangt door weak, kan de retain count van elk object worden gereset. Na het vrijgeven van het object wordt de weak-referentie automatisch op nil gezet, waardoor toegang tot dood geheugen wordt voorkomen.

Kan een retain cycle bestaan uit drie of meer objecten?

Ja, een retain cycle kan elk aantal objecten omvatten: A → B → C → A. Om vrij te geven is het voldoende om één schakel in de cyclus te verbreken — vervang een strong-referentie door weak of unowned. Hulpmiddelen tonen de hele graaf, niet alleen paren van objecten.

Waarom creëert GCD DispatchWorkItem geen retain cycle?

GCD (Grand Central Dispatch) slaat de sluiting niet op na uitvoering. DispatchWorkItem wordt uitgevoerd en vrijgegeven, zelfs als de sluiting self vastlegt. Een retain cycle ontstaat alleen wanneer de sluiting als eigenschap wordt opgeslagen (completion handler in een klasse), niet wanneer deze naar een wachtrij wordt gestuurd.

Welke typen retain cycle worden niet gedetecteerd door Instruments?

Instruments Leaks vindt niet altijd tijdelijke retain cycles (die seconden bestaan) en cyclische verwijzingen in C/C++-objecten via bridge. Gebruik voor een grondige controle handmatig Memory Debugger + deinit-logging van alle belangrijke objecten in de scène.

Samenvatting

  • Retain Cycle — gesloten keten van strong-referenties die het vrijgeven van objecten in ARC blokkeert
  • Oorzaken — delegates met strong-referentie, sluitingen die self vastleggen, bidirectionele parent-child-relaties
  • Oplossing — vervang een strong-referentie door weak of unowned om de cyclus te verbreken
  • Sluitingen — opgeslagen completion handlers moeten altijd [weak self] gebruiken
  • Delegates — altijd weak; het delegate-protocol moet overerven van AnyObject
  • Diagnose — Xcode Memory Debugger, Instruments Leaks, deinit-logging
  • Preventie — unidirectionele gegevensstroom, weak delegate, capture list, basisklasse met deinit

We ontwikkelen een mobiele applicatie turnkey

IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.

Bespreek het project

Lees ook