Avant que Swift n'introduise Automatic Reference Counting (ARC), les développeurs géraient manuellement la mémoire avec des appels retain, release et autorelease, ce qui entraînait souvent des fuites ou des pointeurs pendants. ARC de Swift automatise cela au moment de la compilation en insérant des appels de maintien/releases, mais cela a introduit une complexité subtile avec les fermetures, qui sont des types de référence capturant des variables environnantes. Cela a créé une nouvelle classe de problèmes de mémoire spécifique à Swift où deux types de référence pouvaient former une dépendance circulaire indestructible, nécessitant la syntaxe de liste de capture introduite pour fournir un contrôle explicite sur ces sémantiques de capture.
Lorsque une instance de classe stocke une fermeture comme propriété, et que cette fermeture fait référence à self ou à d'autres propriétés d'instance, ARC incrémente le compte de références de l'instance pour la garder vivante pendant toute la durée de vie de la fermeture. Parce que la fermeture est aussi référencée par l'instance, un cycle de rétention émerge : l'instance maintient fortement la fermeture, et la fermeture maintient fortement l'instance. Aucun des comptes de références n'atteint zéro, empêchant l'exécution de deinit et causant une fuite de mémoire pendant toute la durée de vie de l'application.
Swift fournit des listes de capture - des expressions délimitées par des virgules dans des crochets précédant la liste des paramètres de la fermeture - pour modifier le comportement de capture par défaut. Spécifier [weak self] crée une référence faible (optionnelle, devient nil quand désallouée), tandis que [unowned self] crée une référence non propriétaire (suppose l'existence, plante si elle est accessible après désallocation). Pour les valeurs, [x = x] capture la valeur actuelle plutôt que la référence. Cela brise explicitement le cycle de référence forte, permettant à ARC de désallouer l'instance lorsque les références externes sont supprimées.
Exemple de Code :
class DataManager { var completionHandler: ((Data) -> Void)? var data: Data = Data() func fetchData() { // Cycle de rétention : self maintient la fermeture, la fermeture maintient self completionHandler = { newData in self.data = newData // Capture forte de self } } func fetchDataFixed() { // Solution : capture faible completionHandler = { [weak self] newData in guard let self = self else { return } self.data = newData } } deinit { print("DataManager désalloué") } }
Dans une application iOS de production, nous avons implémenté un ProfileViewController qui s'appuyait sur une classe UserService pour récupérer des données de profil de manière asynchrone. Le service exposait une API utilisant des gestionnaires de complétion basés sur des fermetures stockées comme propriétés pour prendre en charge des requêtes annulables. Nous avons observé que naviguer hors de l'écran de profil ne déclenchait jamais le deinit du ViewController, et Instruments rapportait un objet graph mémoriel persistant maintenant la hiérarchie de vue.
Nous avons considéré plusieurs approches architecturales pour résoudre cette fuite.
Nous avons tenté de définir explicitement le gestionnaire de complétion sur nil dans viewWillDisappear. Bien que cela casse techniquement le cycle lorsque l'utilisateur navigue en arrière, cela s'est avéré peu fiable pour des terminaisons abruptes ou des transitions d'état inattendues. Cela fuyait également si la fermeture n'était jamais invoquée et le contrôleur de vue était désalloué par le système sous pression mémoire avant l'événement de disparition. Cette approche nécessitait une programmation défensive excessive et violait le principe de responsabilité unique en obligeant le contrôleur de vue à gérer l'état interne du service.
Nous avons évalué l'utilisation de [unowned self] dans la fermeture pour éviter la surcharge du déballage optionnel. Cela offrait une propreté syntaxique et des avantages d'abstraction à coût nul. Cependant, lors des tests, nous avons découvert des conditions de concurrence où une navigation rapide pouvait désallouer le ViewController pendant que la requête réseau était encore en cours, entraînant des plantages lorsque le rappel tentait d'accéder à l'instance désallouée. Le risque de comportement indéfini en production l'emportait sur les avantages de performance.
Nous avons mis en œuvre [weak self] associé à une vérification guard let self = self else { return } au point d'entrée de la fermeture. Cela gérait en toute sécurité tous les scénarios de cycle de vie : si le contrôleur de vue était désalloué avant que le rappel se déclenche, la référence faible devenait nil, la garde échouait silencieusement, et ARC nettoyait la fermeture par la suite. Bien que cela nécessite légèrement plus de code standard et introduise une petite surcharge de gestion optionnelle, cela garantissait la sécurité mémoire et le fonctionnement sans crash.
Nous avons adopté l'approche de capture faible de manière universelle dans la base de code. Après avoir restructuré l'intégration de UserService pour utiliser [weak self], le débogage du graph mémoire a confirmé que les instances de ProfileViewController se désallouaient immédiatement après la suppression. Le débogueur de graph mémoire de Xcode ne montrait aucune référence forte restante de la fermeture, et la détection de fuites par Instruments signalait zéro fuite dans la fonctionnalité. Ce modèle est devenu notre standard pour toutes les API asynchrones basées sur des fermetures.
Comment la capture d'une instance de struct dans une fermeture diffère-t-elle de la capture d'une instance de classe, et pourquoi les structs ne peuvent-elles pas créer de cycles de rétention ?
De nombreux candidats supposent incorrectement que capturer self dans une fermeture risque toujours des cycles de rétention, quelle que soit le contexte. Les Structs sont des types de valeur dans Swift, ce qui signifie qu'ils sont copiés plutôt que référencés. Lorsque une struct est capturée par une fermeture, ARC copie la valeur de la struct dans la liste de capture de la fermeture (ou capture une référence à la copie immuable en fonction de l'optimisation), mais surtout, la struct n'a pas de compte de références. Comme la fermeture détient la valeur, et non un pointeur vers un objet alloué sur le tas, il n'y a aucune possibilité de référence circulaire entre la fermeture et l'instance de struct originale.
Le danger existe exclusivement lorsque self fait référence à une classe (type de référence), où la fermeture stocke un pointeur vers l'objet sur le tas, incrémentant son compte de références. Comprendre cette distinction est crucial pour décider d'appliquer des modificateurs de liste de capture lorsqu'on travaille avec des structs de vues SwiftUI par rapport à des contrôleurs de vue UIKit.
Quelle est la différence précise entre [weak self] et [unowned self] concernant les suppositions de durée de vie de l'objet, et quand [unowned self] provoque-t-il un plantage ?
Les candidats traitent souvent ces deux termes de manière interchangeable. [weak self] convertit la capture en une WeakReference optionnelle, que ARC met automatiquement à nil lorsque l'objet est désalloué. Y accéder nécessite un déballage optionnel et est sûr même si l'objet meurt. [unowned self] crée une référence non propriétaire qui suppose que l'objet existera pendant toute la durée de la fermeture ; elle se comporte comme une option qui est implicitement déballée et qui n'est jamais mise à nil.
Si la fermeture survit à l'objet (par exemple, un gestionnaire de complétion stocké appelé après le retrait du contrôleur de vue), accéder à self déréférence un pointeur pendu, provoquant un plantage EXC_BAD_ACCESS. Utilisez [unowned self] uniquement lorsque la fermeture et l'objet ont des durées de vie identiques, comme des fermetures non échappatoires ou des motifs de délégué spécifiques où la fermeture ne peut pas survivre au capturé.
Comment les listes de capture interagissent-elles avec les variables déclarées en dehors de la portée de la fermeture, et [x] crée-t-il une copie ou une référence pour les types de valeur ?
Une idée reçue commune est que les listes de capture n'affectent que self. Lorsque vous écrivez { [x] in ... }, vous capturez explicitement la valeur actuelle de x au moment de la création de la fermeture, créant efficacement une copie d'ombre immuable dans la fermeture. Sans la liste de capture, la fermeture capture une référence à l'emplacement de stockage de la variable originale, lui permettant de voir les mutations apportées après la création de la fermeture et pouvant potentiellement participer à une logique circulaire si x est un type de référence.
Pour les types de valeur comme Int ou String, [x] capture une copie, empêchant la fermeture d'observer les changements externes à x et garantissant que le comportement de la fermeture est déterminé en fonction de l'état au moment de la capture.