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 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.
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.
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.
// 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 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.
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.
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.
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 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.
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 (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 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.
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.
| Outil | Type | Quand l’utiliser |
|---|---|---|
| Memory Debugger | Graphe visuel | Vérification manuelle après navigation |
| Instruments Leaks | Analyse automatisée | Tests de régression, CI |
| deinit print | Journalisation manuelle | Développement, revue de code |
| Malloc Scribble | Indicateur d’exécution | Dé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é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.
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.
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.
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.
// 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
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.
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.
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.
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.
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é
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.
Lisez aussi