Retain Cycle — essence, causes et élimination dans le développement d'applications

Auteur : IT Sectr Publié le : 2026-03-29 Temps de lecture : 8 min

Un Retain Cycle est une situation dans ARC où deux objets ou plus se référencent mutuellement via des références fortes, formant une boucle fermée. Selon le Apple Memory Management Guide, 2026, un retain cycle bloque la libération de tous les objets dans le cycle car chacun a un retain count ≥ 1. Contrairement à une fuite mémoire en GC, un retain cycle garantit que les objets restent vivants tant qu’au moins un participant externe du cycle est vivant — et même après la perte de toutes les références externes, si le cycle est isolé.

Points clés

  • Retain Cycle — une chaîne fermée de références fortes empêchant ARC de libérer les objets
  • Cause — deux (ou plus) objets maintiennent des références fortes l’un vers l’autre, mise à zéro du retain count impossible
  • Conséquence — fuite mémoire : les objets restent en mémoire indéfiniment, la consommation de RAM augmente
  • Solution — remplacer l’une des références fortes dans le cycle par weak ou unowned
  • Diagnostic — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Qu’est-ce que Retain Cycle ?

Retain Cycle est une situation dans laquelle deux objets ou plus se possèdent mutuellement via des références fortes, créant un graphe de dépendances fermé. ARC ne peut libérer aucun de ces objets car le retain count de chacun est toujours ≥ 1 : l’objet A retient B, B retient A, et leurs compteurs n’atteignent jamais zéro.

Le problème survient exclusivement dans les systèmes à comptage de références (ARC, MRR). Dans le Garbage Collection, le collecteur détermine l’inaccessibilité via le graphe de références à partir de l’ensemble racine — les cycles ne sont pas un obstacle. Dans ARC, cependant, un cycle équivaut à une fuite, car la libération déterministe par comptage ne peut pas résoudre les dépendances circulaires.

Selon la WWDC 2012 Session 406, le retain cycle est la cause la plus courante de fuites mémoire dans les applications Objective-C et Swift. Scénarios typiques : relations parent-enfant avec délégations, closures capturant self, et architectures en couches avec relations bidirectionnelles.

Exemples de retain cycle dans le développement iOS

Examinons les scénarios classiques de retain cycle que tout développeur iOS rencontre. Comprendre ces modèles est la base pour écrire du code sécurisé avec ARC.

Parent-Child avec délégation

Scénario classique : un objet parent (par exemple, UIViewController) crée un objet enfant et devient son délégué. Si les deux utilisent des références fortes, un retain cycle se produit. La solution — le délégué doit être weak.

swift
// ERREUR : retain cycle via délégué fort
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 par défaut
}

// CORRECTION : délégué weak
class ChildVC: UIViewController {
    weak var delegate: ChildDelegate? // ✅ weak — ne retient pas
}

Dans l’exemple, ParentVC maintient une référence forte vers ChildVC via la propriété child. ChildVC maintient une référence forte vers ParentVC via delegate. Le cycle est fermé. Correction : weak var delegate — la référence n’augmente pas le retain count, et ParentVC peut être libéré.

NSTimer et retain cycle

NSTimer est une source classique de retain cycles. Le timer retient sa cible (généralement self), et la cible retient le timer via une propriété. Même si le timer est à usage unique, il ne sera pas libéré tant qu’invalidate n’est pas appelé. Solution : toujours appeler timer.invalidate() dans deinit ou viewDidDisappear.

Architectures en couches

Dans les architectures à propriété en cascade (coordinateurs, routeurs), des cycles à plusieurs étapes se produisent souvent : Coordinator → ViewController → ViewModel → Coordinator (via un callback). Chaque référence forte dans la chaîne doit être choisie consciemment — une référence weak à n’importe quel maillon brise le cycle.

Retain Cycle dans les closures Swift

Les closures en Swift capturent les variables externes par référence forte. Si une closure est stockée comme propriété d’un objet (par exemple, un completion handler) et capture self, elle crée un retain cycle : self → closure → self.

C’est la source la plus courante de retain cycles dans le développement Swift moderne. Cela se produit implicitement — un développeur peut ne pas remarquer la capture de self dans une closure, surtout lorsqu’il utilise une syntaxe abrégée sans self explicite.

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()
        }

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

    func notifyUI() { }
}

Une liste de capture [weak self] crée une référence faible à self à l’intérieur de la closure. Si DownloadService est libéré avant l’exécution de la closure, self devient nil, et le code se termine en toute sécurité via guard. C’est un modèle standard pour les closures asynchrones en Swift — il doit être utilisé chaque fois qu’une closure est stockée comme propriété.

Unowned self dans les closures

unowned self est une alternative à weak self lorsque self est garanti de vivre plus longtemps que la closure. Exemple : les closures synchrones qui s’exécutent immédiatement (sorted, filter). Dans ces cas, self est définitivement vivant, et unowned est sûr. Cependant, unowned plante lors de l’accès à un objet libéré — donc weak est considéré comme le choix sûr par défaut.

Comment détecter un retain cycle : outils de diagnostic

Détecter les retain cycles à un stade précoce est crucial pour les performances de l’application. Passons en revue les principaux outils et techniques pour identifier les références cycliques dans le développement iOS.

Xcode Memory Debugger

Xcode Memory Debugger (Debug Memory Graph) est un outil visuel qui montre le graphe des objets en mémoire avec leurs références. Un retain cycle apparaît comme une chaîne fermée de flèches fortes. Pour lancer : cliquez sur le bouton Debug Memory Graph dans le panneau Debug area pendant l’exécution de l’application. Chaque objet est affiché avec son type, son adresse et la liste de ses références.

Instruments Leaks

Instruments Leaks est un profileur pour la détection automatique des fuites. Il enregistre les allocations et analyse le graphe de références en temps réel. Il détecte non seulement les retain cycles, mais aussi les références oubliées, les ViewControllers non libérés et autres fuites. Leaks indique l’objet exact et la chaîne de rétention.

Journalisation de deinit

La méthode la plus simple consiste à ajouter un print dans le deinit de chaque classe clé. Si deinit n’est pas appelé alors que l’objet devrait être détruit, il y a un retain cycle. Cette méthode ne nécessite aucun outil et est efficace pour le diagnostic initial.

OutilTypeQuand l’utiliser
Memory DebuggerGraphe visuelVérification manuelle après navigation
Instruments LeaksAnalyse automatiséeTests de régression, CI
deinit printJournalisation manuelleDéveloppement, revue de code
Malloc ScribbleIndicateur d’exécutionDébogage use-after-free

Approche recommandée : utilisez la journalisation de deinit pendant le développement, Memory Debugger lors des tests manuels, et Instruments Leaks dans le pipeline CI/CD pour la détection automatisée des régressions de fuites.

Prévention du retain cycle et meilleures pratiques

Prévenir les retain cycles est plus facile que de les corriger en production. Voici quelques règles qui minimisent le risque de références cycliques.

Règle du délégué weak

Tous les délégués et dataSources doivent être weak. Cette règle est intégrée dans UIKit : tous les protocoles de délégation dans le SDK Apple sont déclarés avec des propriétés weak (UITableView.delegate, UICollectionView.dataSource). Pour vos propres protocoles, utilisez weak var delegate: MyDelegate? et faites hériter le protocole de AnyObject.

Liste de capture dans les closures

Toute closure qui est stockée comme propriété (completion handler, callback) et qui capture self doit utiliser [weak self] dans la liste de capture. L’exception concerne les closures qui s’exécutent immédiatement et ne sont pas stockées (sorted, map, filter). Pour celles-ci, unowned self est sûr.

Revue d’architecture

Dans les architectures complexes (VIPER, Coordinators, Redux), suivez la direction des références fortes. Le propriétaire maintient une référence forte sur le subordonné, mais le subordonné ne doit référencer le propriétaire que par weak ou unowned. Le flux de données unidirectionnel simplifie la gestion des références.

swift
// Exemple : vérification avec journalisation de deinit
class BaseViewController: UIViewController {
    deinit {
        print("✅ \(type(of: self)) deallocated")
    }
}

// Utilisation : tous les ViewController héritent de BaseViewController
class ProfileVC: BaseViewController {
    var viewModel: ProfileViewModel?
    var onLogout: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        onLogout = { [weak self] in
            self?.dismiss(animated: true)
        }
    }
}
// En fermant ProfileVC, nous attendons « ✅ ProfileVC deallocated » dans la console

Une classe de base avec journalisation de deinit fournit un retour instantané. Si le message n’apparaît pas lorsque l’écran devrait se fermer, il y a un retain cycle dans cette classe. Ajoutez cette pratique au modèle de projet pour tous les ViewControllers.

Foire aux questions

En quoi un retain cycle diffère-t-il d’une fuite mémoire en GC ?

Retain cycle est un problème spécifique à ARC où une boucle fermée de références fortes bloque la libération. En GC, le collecteur analyse l’accessibilité depuis l’ensemble racine, et non les compteurs de référence — donc les cycles ne sont pas des fuites. Dans ARC, cependant, tout cycle isolé est une fuite garantie.

Comment une référence weak brise-t-elle un retain cycle ?

Une référence weak n’augmente pas le retain count d’un objet. Si vous remplacez l’une des références fortes dans un cycle par weak, le retain count de chaque objet peut atteindre zéro. Après la libération de l’objet, la référence weak est automatiquement définie à nil, empêchant l’accès à la mémoire libérée.

Un retain cycle peut-il comprendre trois objets ou plus ?

Oui, un retain cycle peut inclure n’importe quel nombre d’objets : A → B → C → A. Pour le briser, vous n’avez besoin que de casser un maillon dans le cycle — remplacez n’importe quelle référence forte par weak ou unowned. Les outils montrent le graphe complet, pas seulement des paires d’objets.

Pourquoi GCD DispatchWorkItem ne crée-t-il pas de retain cycle ?

GCD (Grand Central Dispatch) ne stocke pas la closure après l’exécution. Le DispatchWorkItem est exécuté et libéré, même si la closure capture self. Un retain cycle se produit uniquement lorsqu’une closure est stockée comme propriété (completion handler dans une classe), pas lorsqu’elle est passée à une file d’attente.

Quels types de retain cycle ne sont pas détectés par Instruments ?

Instruments Leaks ne trouve pas toujours les retain cycles temporaires (durant quelques secondes) ni les références cycliques dans les objets C/C++ via le bridging. Pour une vérification complète, utilisez Memory Debugger manuellement avec la journalisation de deinit de tous les objets clés de la scène.

Résumé

  • Retain Cycle — une chaîne fermée de références fortes bloquant la libération d’objets dans ARC
  • Causes — délégués avec référence forte, closures capturant self, relations parent-enfant bidirectionnelles
  • Solution — remplacer une référence forte par weak ou unowned brise le cycle
  • Closures — les completion handlers stockés doivent toujours utiliser [weak self]
  • Délégués — toujours weak ; le protocole de délégation doit hériter de AnyObject
  • Détection — Xcode Memory Debugger, Instruments Leaks, journalisation de deinit
  • Prévention — flux de données unidirectionnel, délégués weak, listes de capture, classe de base avec deinit

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi