Hop hop hop, tu ne voulais pas nous parler d'un truc important encore ?!
Ah si ! Quand je vous ai présenté le protocole UITableViewDataSource
, on a d'une part limité ce protocole à des classes Objective-C, en adossant NSObjectProtocol
à la déclaration de notre protocole :
protocol UITableViewDataSource: NSObjectProtocol {
// (...)
}
Et dans notre TableView, je vous ai dit que l'on avait notre objet dataSource
, mais avec le mot réservé weak
:
class UITableView: UIScrollView {
weak var dataSource: UITableViewDataSource?
}
Alors, pourquoi a-t-on fait tout cela ? Qu'est-ce que cela veut dire ? On va rapidement toucher au monde des références pour comprendre tout cela.
Les références
Dans les langages de programmation un peu modernes (comme Swift !), les objets que vous créez restent dans la mémoire, tant qu'au moins une référence existe vers cet objet.
Quand mon objet n'a plus aucun autre objet ayant une référence sur lui, pouf ! Il disparaît ! Sous iOS, la technologie qui fait tout cela s'appelle Automatic Reference Counting (ARC).
Dans d'autres langages, on parle de Garbage Collector pour désigner cette technologie : le programme nettoie la mémoire en enlevant les objets qui ne servent plus à rien. Et comment sait-on qu'un objet ne sert plus à rien ? Quand il n'a plus de références ! Dans ce cas, mon programme n'a plus aucun moyen d'accéder à cet objet, et celui-ci est considéré comme perdu.
Tout cela ne s'applique qu'aux classes ; les structures et les énums ne sont pas concernées.
Euh… mais pourquoi ?
Tout simplement parce que c’est comme ça qu'est conçu le Swift. Les classes sont naturellement passées par référence, et les structures et les énumérations sont passées par copie.
Par copie ? 😜
Oui ! À chaque fois qu’une variable de type structure ou énumération est passée d’un objet à un autre, il s’agit en fait d’une copie de cet objet qui est faite en mémoire, et non du même objet réellement !
L’utilisation du protocole NSObjectProtocol
garantit que l’objet qui implémente le protocole sera une classe, car les classes Objective-C ne sont compatibles qu’avec des classes Swift. Il n’existe pas de structure ou d'énumération en Objective-C.
Sachez que vous pouvez aussi définir la conformance à une classe directement avec le mot clé class
dans la déclaration de notre protocole. L’effet sera identique, si ce n’est que ça n’oblige pas d’avoir une classe Objective-C derrière.
Ici, ma TableView et mon bouton restent bien dans la mémoire, puisque j'ai au moins un objet qui les référence, mon View Controller. Et celui-ci reste aussi dans la mémoire, puisqu'il a également une référence... Et ainsi de suite.
OK, mais tout cela ne me dit pas ce qu'est une référence !
Une référence, c'est très simple ! Quand j'écris ceci :
class ListViewController: UIViewController {
var tableView: UITableView
}
… je crée une référence de mon ListViewController
vers ma propriété tableView
. Tant que mon contrôleur est dans la mémoire (et généralement, il y reste tant qu'il est présent dans ma navigation), ma TableView est là aussi. C'est bien rassurant, finalement !
Le mot-clé weak
Si on reprend notre exemple de delegate de tout à l'heure, ma TableView s'écrit comme cela, si j'enlève ce fameux mot weak
:
class UITableView: UIScrollView {
var dataSource: UITableViewDataSource?
}
Donc en termes de référence, quand j'écris après dans mon View Controller tableView.dataSource = self
, cela donne ça :
Et là… c'est le drame !
Pourquoi c’est le drame ? 😰
Parce que, sans faire attention, j'ai créé un retain cycle. En fait, chaque objet a une référence vers l'autre. Même si mon View Controller n'est plus dans la navigation, et qu'aucun objet n'a de référence vers lui, le couple View Controller <> TableView ne disparaîtra jamais, car chacun a au moins une référence, celle de l'autre objet du couple. Cela crée finalement une fuite mémoire.
Mais que va-t-on bien pouvoir faire ??! 🥺
Pas de panique ! Vous l'aurez sans doute compris, c'est là que le mot weak
entre en jeu ! Pour comprendre weak
, on va d'abord regarder ce que fait son contraire : strong
.
Par défaut, quand je déclare une propriété sur un objet de type Objet
:
var monObjet: Objet
Cela équivaut à écrire :
strong var monObjet: Objet
Ma référence vers mon instance de Objet
doit être forte pour maintenir mon objet dans la mémoire. Quand le nombre de références fortes vers mon objet tombe à zéro, il n'y a plus rien pour le garder dans la mémoire, et c'est là qu'il disparaît.
Donc quand je prépose weak à la déclaration de ma variable, j'indique que je veux une référence faible. Et au contraire d'une référence forte, une référence faible ne retient pas mon objet dans la mémoire ! Je peux accéder à mon objet dans la mémoire, mais ce n'est pas moi qui le retiendrai. Je ne vais pas le retenir s'il doit disparaître.
Autrement dit, lorsque ARC compte les références pour savoir si un objet doit être supprimé de la mémoire, il ne compte que les références fortes, les faibles ne comptant pas.
Si on reprend notre schéma de tout à l'heure, avec la déclaration en weak
, cela donne ça :
Et voilà le travail, le mot weak permet de briser ce fameux retain cycle ! Quand mon contrôleur ne sera plus dans la navigation, et qu'il n'aura plus de référence vers lui, ma tableView
ne l'empêchera pas d'être enlevé de la mémoire.
Retour sur les outlets
Ouais, mais on ne me la fait pas. Ton schéma est faux, on a déclaré notre TableView avec un IBOutlet et avec le mot clé weak
.
C'est vrai. Quand on déclare un outlet avec weak
, en théorie, notre objet ne devrait même pas pouvoir rester dans la mémoire, puisqu'il n'y a aucune référence strong
pour le retenir, non ?
Bien sûr, c'est faux, sinon nos apps ne fonctionneraient pas depuis le début, hé hé !
Alors, que se passe-t-il au juste en réalité ? Eh bien, il suffit de se souvenir que tout bon UIViewController
gère une vue principale. Si on reprend notre schéma, dans la réalité, on a ceci :
La vue principale maintient un lien fort sur l'ensemble de ses sous-vues via la propriété subviews
. Et tant que mon contrôleur est présent, il maintient aussi un lien fort sur sa vue principale via la propriété view
. Donc ma tableView
reste bien dans la mémoire.
On déclare donc nos outlets en weak
pour éviter une redondance, ou pour éviter des problèmes si on crée des liens entre objets qui n'ont rien à voir ! Depuis le temps que l'on déclare des outlets, cela devait vous démanger de ne pas savoir, non ?
Si tout cela vous paraît compliqué, pas de panique. Retenez simplement le concept de retain cycle, et que si deux objets s'autoréférencent, vous allez avoir des problèmes de mémoire. Lorsque vous créerez vos propres delegates, pensez à les indiquer en weak
pour éviter ce problème, et tout ira bien !
En résumé
Un objet reste dans la mémoire, tant qu'au moins une référence forte existe vers cet objet.
Si deux objets s'autoréférencent avec des références fortes, cela crée un retain cycle : mes objets ne peuvent plus être nettoyés de la mémoire.
Le mot réservé weak permet de résoudre ce problème : les références dites faibles ne comptent pas lorsque le programme détermine si l'objet est encore utile ou pas.
On déclare un delegate en
weak
pour éviter de créer un cycle de rétention et donc une fuite mémoire.Les propriétés avec des
IBOutlets
sont déclarées enweak
: en général, l'objet aura déjà une référence forte interne, et en rajouter une autre ne servira pas.
Prêt à passer à l'action ? Dans le chapitre suivant, vous allez vous entraîner à réaliser une application… listant les 50 meilleurs films du monde ! :soleil: