ARC en Swift : guide technique complet (weak, unowned, cycles et détails internes)
Swift utilise ARC (Automatic Reference Counting) pour gérer automatiquement la mémoire
des objets de type class (références). ARC n’est pas un garbage collector : il libère
un objet immédiatement quand son compteur de références atteint zéro.
Comprendre ARC est indispensable pour :
- éviter les cycles de rétention (memory leaks),
- choisir correctement entre
strong,weaketunowned, - écrire des closures sûres (capture lists),
- raisonner sur les graphes d’objets dans UIKit/SwiftUI/Combine.
1. Rappels : valeur vs référence
En Swift :
- Value types :
struct,enum(copie, pas gérés par ARC au niveau “objet”). - Reference types :
class, certains types runtime Objective-C (gérés par ARC).
ARC s’applique essentiellement aux objets alloués sur le heap (instances de classes).
2. Comment ARC fonctionne (modèle mental)
Chaque objet référence possède un compteur de références :
- quand une nouvelle référence strong pointe vers l’objet ? compteur +1,
- quand une référence strong disparaît (fin de scope, affectation, etc.) ? compteur -1,
- quand compteur = 0 ? l’objet est désalloué immédiatement (
deinitappelé).
Exemple :
final class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("deinit Person:", name) }
}
do {
let p = Person(name: "Alice") // refcount = 1
// fin du scope -> refcount = 0 -> deinit
}
3. strong par défaut
En Swift, toute propriété de type référence est strong par défaut :
final class Owner {
var child: Child? // strong par défaut
}
Cela signifie : “je possède cet objet, je prolonge sa durée de vie”.
4. Le problème : cycles de rétention
Un cycle de rétention (retain cycle) apparaît quand deux objets se retiennent mutuellement via des références strong. Résultat : leur refcount ne tombe jamais à zéro ? fuite mémoire.
final class A {
var b: B?
deinit { print("deinit A") }
}
final class B {
var a: A?
deinit { print("deinit B") }
}
func leak() {
let a = A()
let b = B()
a.b = b
b.a = a
// Fin de fonction : a et b restent en mémoire, cycle strong-strong
}
5. weak : référence non possédante et “zeroing”
weak est une référence non possédante :
elle n’augmente pas le refcount.
Quand l’objet est libéré, la référence weak est automatiquement mise à nil
(zeroing weak reference).
En Swift, une propriété weak doit être optionnelle
(car elle peut devenir nil à tout moment).
final class Parent {
var child: Child?
}
final class Child {
weak var parent: Parent? // pas de cycle
}
Ici :
Parentpossède fortementChild,ChildréférenceParentsans le posséder.
Quand le parent est libéré, child.parent devient nil.
5.1 Coût et implémentation interne (conceptuel)
Les références weak impliquent généralement une structure interne (souvent appelée “side table”) pour suivre et “zero out” toutes les références weak quand l’objet est désalloué.
- Une référence strong est très rapide (incrément/décrément).
- Une référence weak implique un mécanisme supplémentaire pour la mise à zéro.
En pratique, c’est rarement un problème, mais dans des hot paths, on évite des millions de weak si ce n’est pas nécessaire.
6. unowned : référence non possédante mais non optionnelle
unowned n’augmente pas le refcount non plus, mais contrairement à weak :
- la référence est en général non optionnelle,
- elle n’est pas automatiquement nil,
- si on y accède après désallocation ? crash (dangling pointer).
On utilise unowned uniquement quand on est certain que l’objet référencé
vivra plus longtemps que la référence.
final class Owner {
let name = "Owner"
lazy var worker: Worker = Worker(owner: self)
}
final class Worker {
unowned let owner: Owner
init(owner: Owner) { self.owner = owner }
}
Ici, Worker ne peut pas exister sans Owner,
donc unowned est cohérent (modèle “owner outlives worker”).
6.1 unowned(unsafe)
Swift propose aussi unowned(unsafe) (rarement recommandé) :
il évite certains coûts mais ne vérifie pas la validité ? accès après free = comportement indéfini.
7. weak vs unowned : comment choisir ?
- weak : quand l’objet peut disparaître avant nous (cas général). Pas de crash, devient nil.
- unowned : quand l’objet est garanti vivant tant que nous existons. Non optionnel, mais crash si faux.
Règle pratique :
- Si tu hésites ? choisis
weak. - Choisis
unownedseulement quand la contrainte de durée de vie est structurelle et prouvable.
8. Le cas n°1 des cycles : closures
Les closures capturent par défaut en strong. Si un objet stocke une closure qui capture cet objet ? cycle.
final class Loader {
var onComplete: (() -> Void)?
func start() {
onComplete = {
print("done") // capture self implicite si utilisé
}
}
}
Le problème apparaît surtout quand on capture self :
final class ViewModel {
var onUpdate: (() -> Void)?
func bind() {
onUpdate = {
self.refreshUI() // self capturé strongly
}
}
func refreshUI() {}
}
Si ViewModel garde onUpdate et que onUpdate garde self,
on a un cycle.
8.1 Capture list : [weak self]
onUpdate = { [weak self] in
guard let self else { return }
self.refreshUI()
}
8.2 Capture list : [unowned self]
onUpdate = { [unowned self] in
self.refreshUI()
}
Attention : si la closure peut être appelée après la désallocation,
unowned crash. Exemple typique : callbacks async, timers, publishers.
9. Le cas n°2 : delegate pattern (UIKit)
Le pattern delegate crée très souvent un cycle : le parent possède l’enfant, l’enfant référence le parent comme delegate.
Convention iOS : un delegate est weak.
protocol CellDelegate: AnyObject {
func didTap()
}
final class MyCell {
weak var delegate: CellDelegate?
}
AnyObject est nécessaire pour pouvoir déclarer weak
(weak ne s’applique qu’aux types référence).
10. Détails internes : où est stocké le refcount ?
Conceptuellement, le runtime Swift/ObjC stocke des informations de gestion mémoire associées à chaque objet :
- compteurs strong (retain/release),
- compteurs weak,
- métadonnées (type, flags runtime).
Dans de nombreux cas, des bits de comptage sont stockés “inline” dans l’objet, et des structures auxiliaires (“side tables”) sont utilisées quand :
- il y a des références weak à suivre (zeroing),
- le comptage devient plus complexe (overflow, flags),
- interop Objective-C.
Pour un dev iOS, l’important est le comportement observable :
- strong garde en vie,
- weak ne garde pas en vie et devient nil,
- unowned ne garde pas en vie et peut crasher si mal utilisé.
11. Cas pratiques avancés et pièges
11.1 Timer / CADisplayLink
Les timers retiennent souvent leur target / closure. Il faut casser la chaîne à la fin :
final class Poller {
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
}
func stop() {
timer?.invalidate()
timer = nil
}
private func tick() {}
}
11.2 Combine / NotificationCenter
Les subscriptions peuvent prolonger la vie si on stocke fort des closures capturant self.
Pattern fréquent : [weak self] + annulation explicite.
11.3 SwiftUI
SwiftUI recrée souvent des vues (value types), mais les objets observés (ObservableObject) sont des références. Les cycles apparaissent surtout dans les services / view models / publishers et closures async.
12. Bonnes pratiques professionnelles
- Delegate : presque toujours
weak. - Closures stockées (properties) : penser systématiquement à la capture list.
- Callbacks async : préférer
[weak self](car la tâche peut finir tard). unownedseulement quand la relation de durée de vie est garantie et stable.- Utiliser Instruments (Leaks, Allocations) pour valider en réel.
Conclusion
ARC rend la gestion mémoire largement automatique en Swift, mais il ne peut pas “deviner” l’intention architecturale : les cycles strong-strong restent possibles.
Comprendre weak et unowned, savoir où les utiliser,
et reconnaître les patterns à risque (closures, delegates, timers, subscriptions)
est une compétence essentielle pour produire des apps iOS stables et performantes.