Swift: ARC et gestion de la mémoire

Étant un langage moderne de haut niveau, Swift s'occupe essentiellement de la gestion de la mémoire dans vos applications, allouant et libérant de la mémoire. Cela est dû à un mécanisme appelé Automatic Reference Counting , ou ARC pour faire court. Dans ce guide, vous apprendrez comment ARC fonctionne et comment gérer correctement la mémoire dans Swift. En comprenant ce mécanisme, vous pouvez influencer la durée de vie des objets situés sur le tas ( tas ).

Dans ce guide, vous approfondirez vos connaissances sur Swift et ARC en apprenant ce qui suit:

  • comment fonctionne ARC
  • quels sont les cycles de référence et comment les corriger correctement
  • comment créer un exemple de boucle de lien
  • Comment trouver des boucles de liens en utilisant les outils visuels offerts par Xcode
  • comment gérer les types de référence et les types de valeur

Pour commencer


Téléchargez le matériel source. Ouvrez le projet dans le dossier Cycles / Starter . Dans la première partie de notre guide, comprenant les concepts clés, nous traiterons exclusivement du fichier MainViewController.swif t.

Ajoutez cette classe au bas de MainViewController.swift:

class User { let name: String init(name: String) { self.name = name print("User \(name) was initialized") } deinit { print("Deallocating user named: \(name)") } } 

La classe User est définie ici, ce qui, à l'aide d'instructions print , nous informe de l'initialisation et de la libération de l'instance de classe.

Créez maintenant une instance de la classe User en haut de MainViewController.

Placez ce code avant la méthode viewDidLoad () :

 let user = User(name: "John") 

Lancez l'appli. Rendez la console Xcode visible avec Command-Shift-Y pour voir la sortie des instructions d'impression.

Notez que l' utilisateur John a été initialisé est apparu sur la console, mais l'instruction print dans deinit n'a pas été exécutée. Cela signifie que cet objet n'a pas été libéré, car il n'est pas sorti de son champ d' application .

En d'autres termes, tant que le contrôleur de vue contenant cet objet ne sera pas hors de portée, l'objet ne sera jamais libéré.

Est-il dans la portée?


En encapsulant une instance de la classe User dans une méthode, nous lui permettons de sortir de la portée, permettant ainsi à ARC de la libérer.

Créons la méthode runScenario () dans la classe MainViewController et déplaçons l'initialisation de l'instance de classe User à l'intérieur.

 func runScenario() { let user = User(name: "John") } 

runScenario () définit la portée de l'instance User. En quittant cette zone, l' utilisateur doit être libéré.

Appelez maintenant runScenario () en ajoutant ceci à la fin de viewDidLoad ():

 runScenario() 

Lancez l'appli. La sortie de la console ressemble maintenant à ceci:

L'utilisateur John a été initialisé
Utilisateur de désallocation nommé: John

Cela signifie que vous avez libéré un objet qui a quitté le champ de vision.

Durée de vie de l'objet



L'existence de l'objet est divisée en cinq étapes:

  • allocation de mémoire: à partir de la pile ou du tas
  • initialisation: le code est exécuté dans init
  • utilisation de
  • deinitialisation: le code est exécuté dans deinit
  • mémoire libre: la mémoire allouée est retournée à la pile ou au tas

Il n'y a aucun moyen direct de suivre les étapes d'allocation et de libération de mémoire, mais vous pouvez utiliser le code dans init et deinit.

Les décomptes de référence , également appelés décomptes d'utilisation , déterminent quand un objet n'est plus nécessaire. Ce compteur indique le nombre de ceux qui "utilisent" cet objet. Un objet devient inutile lorsque le compteur d'utilisation est nul. Ensuite, l'objet est désinitialisé et libéré.



Lorsque l'objet utilisateur est initialisé, son nombre de références est 1, car la constante utilisateur fait référence à cet objet.

À la fin de runScenario (), l'utilisateur sort de la portée et le nombre de références est réduit à 0. Par conséquent, l'utilisateur n'est pas initialisé, puis libéré.

Cycles de référence


Dans la plupart des cas, ARC fonctionne comme il se doit. Le développeur n'a généralement pas à s'inquiéter des fuites de mémoire lorsque les objets inutilisés restent non alloués indéfiniment.

Mais pas toujours! Fuites de mémoire possibles.

Comment cela peut-il arriver? Imaginez une situation où deux objets ne sont plus utilisés, mais chacun se réfère à l'autre. Étant donné que chaque compte de référence n'est pas 0, aucun d'entre eux ne sera libéré.



Il s'agit d'un solide cycle de référence . Cette situation confond l'ARC et ne lui permet pas d'effacer la mémoire.

Comme vous pouvez le voir, le nombre de références à la fin n'est pas 0, et bien qu'aucun objet ne soit plus nécessaire, object1 et object2 ne seront pas libérés.

Consultez nos liens


Pour tester tout cela en action, ajoutez ce code après la classe User dans MainViewController.swift:

 class Phone { let model: String var owner: User? init(model: String) { self.model = model print("Phone \(model) was initialized") } deinit { print("Deallocating phone named: \(model)") } } 

Ce code ajoute une nouvelle classe Phone avec deux propriétés, une pour le modèle et une pour le propriétaire, ainsi que les méthodes init et deinit. La propriété du propriétaire est facultative, car le téléphone peut ne pas avoir de propriétaire.

Ajoutez maintenant cette ligne à runScenario ():

 let iPhone = Phone(model: "iPhone Xs") 

Cela va créer une instance de la classe Phone.

Tenez le mobile


Ajoutez maintenant ce code à la classe User, immédiatement après la propriété name:

 private(set) var phones: [Phone] = [] func add(phone: Phone) { phones.append(phone) phone.owner = self } 

Ajoutez un tableau de téléphones appartenant à l'utilisateur. Le setter est marqué comme privé, donc ajoutez (téléphone :) doit être utilisé.

Lancez l'appli. Comme vous pouvez le voir, les instances des classes d'objets Phone et User sont libérées selon les besoins.

L'utilisateur John a été initialisé
Le téléphone iPhone XS a été initialisé
Désallocation du téléphone nommé: iPhone Xs
Utilisateur de désallocation nommé: John

Ajoutez maintenant ceci à la fin de runScenario ():
 user.add(phone: iPhone) 


Ici, nous ajoutons notre iPhone à la liste des téléphones appartenant à l' utilisateur , et définissons également la propriété du propriétaire du téléphone sur « utilisateur ».

Exécutez à nouveau l'application. Vous verrez que les objets utilisateur et iPhone ne sont pas libérés. Le cycle de liens forts entre eux empêche l'ARC de les libérer.



Liens faibles


Pour briser le cycle des liens forts, vous pouvez désigner la relation entre les objets comme faible.

Par défaut, tous les liens sont solides et l'affectation entraîne une augmentation du nombre de références. Lorsque vous utilisez des références faibles, le nombre de références n'augmente pas.

En d'autres termes, les maillons faibles n'affectent pas la gestion de la vie d'un objet . Les liens faibles sont toujours déclarés facultatifs . De cette façon, lorsque le nombre de liens devient 0, le lien peut être défini sur zéro.



Dans cette illustration, les lignes pointillées indiquent des maillons faibles. Notez que le nombre de références de object1 est 1, car variable1 s'y réfère. Le nombre de références de object2 est 2, car il est référencé par variable2 et object1.

object2 fait également référence à object1, mais FAIBLE , ce qui signifie qu'il n'affecte pas le nombre de références de object1.

Lorsque variable1 et variable2 sont libérées, object1 a un compte de référence de 0, ce qui le libère. Ceci, à son tour, libère une référence forte à object2, ce qui conduit déjà à sa libération.

Dans la classe Phone, modifiez la déclaration de propriété du propriétaire comme suit:

 weak var owner: User? 

En déclarant la référence de propriété propriétaire comme «faible», nous rompons la boucle des liens solides entre les classes Utilisateur et Téléphone.



Lancez l'appli. L'utilisateur et le téléphone sont maintenant correctement libérés.

Liens sans propriétaire


Il existe également un autre modificateur de lien qui n'augmente pas le nombre de références: sans propriétaire .

Quelle est la difference entre unowned et un faible ? Une référence faible est toujours facultative et devient automatiquement nulle lorsque l'objet référencé est libéré.

C'est pourquoi nous devons déclarer les propriétés faibles comme une variable optionnelle de type: cette propriété doit changer.

Les liens non possédés, en revanche, ne sont jamais facultatifs. Si vous essayez d'accéder à une propriété sans propriétaire qui fait référence à un objet libéré, vous obtiendrez une erreur qui ressemble à un déballage forcé contenant une variable nil (débouclage forcé).



Essayons d'appliquer sans propriétaire .

Ajoutez une nouvelle classe CarrierSubscription à la fin de MainViewController.swift:

 class CarrierSubscription { let name: String let countryCode: String let number: String let user: User init(name: String, countryCode: String, number: String, user: User) { self.name = name self.countryCode = countryCode self.number = number self.user = user print("CarrierSubscription \(name) is initialized") } deinit { print("Deallocating CarrierSubscription named: \(name)") } } 

CarrierSubscription a quatre propriétés:

Nom: nom du fournisseur.
CountryCode: code du pays.
Numéro: numéro de téléphone.
Utilisateur: lien vers l'utilisateur.

Quel est votre fournisseur?


Ajoutez maintenant ceci à la classe User après la propriété name:

 var subscriptions: [CarrierSubscription] = [] 

Ici, nous conservons un éventail de fournisseurs d'utilisateurs.

Ajoutez maintenant ceci à la classe Phone, après la propriété du propriétaire:

 var carrierSubscription: CarrierSubscription? func provision(carrierSubscription: CarrierSubscription) { self.carrierSubscription = carrierSubscription } func decommission() { carrierSubscription = nil } 

Cela ajoute la propriété facultative CarrierSubscription et deux méthodes d'enregistrement et de désinscription du téléphone auprès du fournisseur.

Ajoutez maintenant la classe CarrierSubscription dans la méthode init, juste avant l'instruction print:

 user.subscriptions.append(self) 

Nous ajoutons CarrierSubscription à la gamme de fournisseurs d'utilisateurs.

Enfin, ajoutez ceci à la fin de la méthode runScenario ():

 let subscription = CarrierSubscription( name: "TelBel", countryCode: "0032", number: "31415926", user: user) iPhone.provision(carrierSubscription: subscription) 

Nous créons un abonnement au fournisseur pour l'utilisateur et y connectons le téléphone.

Lancez l'appli. Dans la console, vous verrez:

L'utilisateur John a été initialisé
Le téléphone iPhone Xs a été initialisé
CarrierSubscription TelBel est initialisé

Et encore un cycle de liaison! l'utilisateur, l'iPhone et l'abonnement n'étaient pas gratuits à la fin.

Pouvez-vous trouver un problème?



Briser la chaîne


Le lien de l'utilisateur à l'abonnement ou le lien de l'abonnement à l'utilisateur doit être inconnu pour rompre la boucle. La question est de savoir quelle option choisir. Regardons les structures.

Un utilisateur possède un abonnement à un fournisseur, mais vice versa - non, un abonnement à un fournisseur ne possède pas d'utilisateur.

De plus, il n'y a aucun intérêt à l'existence de CarrierSubscription sans référence à l'utilisateur qui en est propriétaire.

Par conséquent, le lien utilisateur doit être sans propriétaire.

Modifiez la déclaration de l'utilisateur dans CarrierSubscription:

 unowned let user: User 

Désormais utilisateur sans propriétaire, ce qui rompt la boucle des liens et vous permet de libérer tous les objets.



Liens de boucle dans les fermetures


Les cycles de liaison pour les objets se produisent lorsque les objets ont des propriétés qui se référencent. Comme les objets, les fermetures sont un type de référence et peuvent conduire à des boucles de référence. Les fermetures capturent les objets qu'ils utilisent.

Par exemple, si vous affectez une fermeture à une propriété d'une classe et que cette fermeture utilise des propriétés de la même classe, nous obtenons une boucle de liens. En d'autres termes, l'objet détient un lien vers la fermeture à travers la propriété. La fermeture contient une référence à l'objet à travers la valeur capturée de soi.



Ajoutez ce code à CarrierSubscription immédiatement après la propriété utilisateur:

 lazy var completePhoneNumber: () -> String = { self.countryCode + " " + self.number } 

Cette fermeture calcule et renvoie le numéro de téléphone complet. La propriété est déclarée paresseuse , elle sera attribuée lors de la première utilisation.

Cela est nécessaire car il utilise self.countryCode et self.number, qui ne seront disponibles que lorsque le code d'initialisation sera exécuté.

Ajoutez runScenario () à la fin:

 print(subscription.completePhoneNumber()) 

L'appel de completePhoneNumber () exécutera la fermeture.

Lancez l'application et vous verrez que l'utilisateur et l'iPhone sont libérés, mais CarrierSubscription ne l'est pas, en raison d'un cycle de liens solides entre l'objet et la fermeture.



Listes de capture


Swift fournit un moyen simple et élégant de rompre la boucle des liens solides lors des fermetures. Vous déclarez une liste de capture dans laquelle vous définissez la relation entre la fermeture et les objets qu'elle capture.

Pour illustrer la liste de capture, tenez compte du code suivant:

 var x = 5 var y = 5 let someClosure = { [x] in print("\(x), \(y)") } x = 6 y = 6 someClosure() // Prints 5, 6 print("\(x), \(y)") // Prints 6, 6 

x est dans la liste de capture de fermeture, donc la valeur de x est copiée dans la définition de fermeture. Il est capturé par la valeur.

y n'est pas dans la liste de capture, il est capturé par référence. Cela signifie que la valeur de y sera ce qu'elle était au moment de l'appel du circuit.

Les listes de verrouillage aident à identifier les interactions faibles ou inconnues par rapport aux objets capturés dans la boucle. Dans notre cas, le choix approprié n'est pas détenu, car une fermeture ne peut pas exister si l'instance CarrierSubscription est libérée.

Saisissez-vous


Remplacez la définition completePhoneNumber par CarrierSubscription ::

 lazy var completePhoneNumber: () -> String = { [unowned self] in return self.countryCode + " " + self.number } 

Nous ajoutons [auto sans propriétaire] à la liste de capture de fermeture. Cela signifie que nous nous sommes capturés comme un lien sans propriétaire au lieu d'un lien fort.

Lancez l'application et vous verrez que CarrierSubscription est maintenant disponible.

En fait, la syntaxe ci-dessus est une forme abrégée d'une syntaxe plus longue et plus complète dans laquelle une nouvelle variable apparaît:

 var closure = { [unowned newID = self] in // Use unowned newID here... } 

Ici, newID est une copie de soi sans propriétaire. Au-delà de la fermeture, le moi reste lui-même. Dans la forme abrégée donnée précédemment, nous créons une nouvelle variable auto qui masque le soi existant à l'intérieur de la fermeture.

Utilisez Unown avec précaution


Dans votre code, la relation entre self et completePhoneNumber est désignée comme non possédée.

Si vous êtes sûr que l'objet utilisé dans la fermeture ne sera pas libéré, vous pouvez l'utiliser sans propriétaire. S'il le fait, vous avez des ennuis!

Ajoutez ce code à la fin de MainViewController.swift:

 class WWDCGreeting { let who: String init(who: String) { self.who = who } lazy var greetingMaker: () -> String = { [unowned self] in return "Hello \(self.who)." } } 

Voici maintenant la fin de runScenario ():

 let greetingMaker: () -> String do { let mermaid = WWDCGreeting(who: "caffeinated mermaid") greetingMaker = mermaid.greetingMaker } print(greetingMaker()) // ! 

Lancez l'application et vous verrez un crash et quelque chose comme ça dans la console:

L'utilisateur John a été initialisé
Le téléphone iPhone XS a été initialisé
CarrierSubscription TelBel est initialisé
0032 31415926
Erreur fatale: tentative de lecture d'une référence non possédée mais l'objet 0x600000f0de30 était déjà désalloué2019-02-24 12: 29: 40.744248-0600 Cycles [33489: 5926466] Erreur fatale: tentative de lecture d'une référence sans propriétaire mais l'objet 0x600000f0de30 était déjà désalloué

Une exception s'est produite car la fermeture attend que self.who existe, mais elle a été libérée dès que la sirène est devenue hors de portée à la fin du bloc do.

Cet exemple peut sembler aspiré d'un doigt, mais de telles choses se produisent. Par exemple, lorsque nous utilisons des fermetures pour démarrer quelque chose beaucoup plus tard, disons après la fin de l'appel asynchrone sur le réseau.

Désamorcer le piège


Remplacez greetingMaker dans la classe WWDCGreeting par ceci:

 lazy var greetingMaker: () -> String = { [weak self] in return "Hello \(self?.who)." } 

Nous avons fait deux choses: premièrement, nous avons remplacé les sans-propriétaire par des faibles. Deuxièmement, puisque le soi est devenu faible, nous accédons à la propriété who par le biais du soi. Ignorez l'avertissement Xcode, nous le corrigerons bientôt.

L'application ne se bloque plus, mais si vous l'exécutez, nous obtenons un résultat amusant: «Bonjour nil».

Peut-être que le résultat est tout à fait acceptable, mais souvent nous devons faire quelque chose si l'objet a été libéré. Cela peut être fait en utilisant la déclaration de garde.

Remplacez le texte de clôture par ceci:

 lazy var greetingMaker: () -> String = { [weak self] in guard let self = self else { return "No greeting available." } return "Hello \(self.who)." } 

La déclaration du garde attribue le soi au soi faible. Si l'auto est nul, la fermeture renvoie «Aucun message d'accueil disponible». Sinon, le moi devient une référence forte, donc l'objet est garanti de vivre jusqu'à la fin de la fermeture.

Recherche de boucles de lien dans Xcode 10


Maintenant que vous comprenez comment ARC fonctionne, ce que sont les boucles de liens et comment les rompre, il est temps de voir un exemple d'application réelle.

Ouvrez le projet Starter situé dans le dossier Contacts.

Lancez l'appli.



Il s'agit du gestionnaire de contacts le plus simple. Essayez de cliquer sur un contact, ajoutez-en quelques nouveaux.

Affectation des fichiers:

ContactsTableViewController: affiche tous les contacts.
DetailViewController: affiche les informations détaillées du contact sélectionné.
NewContactViewController: permet d'ajouter un nouveau contact.
ContactTableViewCell: cellule de tableau affichant les coordonnées.
Contact: modèle de contact.
Numéro: modèle de numéro de téléphone.

Cependant, avec ce projet, tout va mal: il y a un cycle de liens. Au début, les utilisateurs ne remarqueront pas de problèmes en raison de la petite taille de la mémoire qui fuit, pour la même raison, il est difficile de trouver la fuite.

Heureusement, Xcode 10 a des outils intégrés pour trouver la plus petite fuite de mémoire.

Relancez l'application. Supprimez 3-4 contacts à l'aide du balayage vers la gauche et du bouton Supprimer. Il semble qu'ils disparaissent complètement, non?



Où coule-t-il?


Lorsque l'application est en cours d'exécution, cliquez sur le bouton Debug Memory Graph:



Observez les problèmes d'exécution dans le navigateur de débogage. Ils sont marqués de carrés violets avec un point d'exclamation blanc à l'intérieur:



Sélectionnez l'un des objets Contact problématiques dans le navigateur. Le cycle est clairement visible: les objets Contact et Numéro, se référant l'un à l'autre, tiennent.



On dirait que vous devriez regarder dans le code. Gardez à l'esprit qu'un contact peut exister sans numéro, mais pas vice versa.

Comment résoudriez-vous cette boucle? Lien du contact au numéro ou du numéro au contact? faible ou sans propriétaire? Essayez-le d'abord!

Si vous aviez besoin d'aide ...
Il existe 2 solutions possibles: rendre un lien de Contact à Numéro faible, ou de Numéro à Contact sans propriétaire.

La documentation d'Apple recommande que l'objet parent ait une référence forte à "enfant" - et non l'inverse. Cela signifie que nous donnons à Contact une référence forte au numéro et au numéro - un lien sans propriétaire vers Contact:

 class Number { unowned var contact: Contact // Other code... } class Contact { var number: Number? // Other code... } 


Bonus: boucles avec types de référence et types de valeur.


Swift possède des types de référence (classes et fermetures) et des types de valeur (structures, énumérations). Le type de valeur est copié lorsqu'il est transmis et les types de référence partagent la même valeur à l'aide du lien.

Cela signifie que dans le cas des types de valeurs, il ne peut pas y avoir de cycles. Pour qu'une boucle se produise, nous avons besoin d'au moins 2 types de référence.

Revenons au projet Cycles et ajoutons ce code à la fin de MainViewController.swift:

 struct Node { // Error var payload = 0 var next: Node? } 

Ne fonctionnera pas! La structure est un type de valeur et ne peut pas avoir de récursivité sur une instance d'elle-même. Sinon, une telle structure aurait une taille infinie.

Modifiez la structure en classe.

 class Node { var payload = 0 var next: Node? } 

La référence à elle-même est tout à fait acceptable pour les classes (type de référence), donc le compilateur n'a pas de problèmes.

Ajoutez maintenant ceci à la fin de MainViewController.swift:

 class Person { var name: String var friends: [Person] = [] init(name: String) { self.name = name print("New person instance: \(name)") } deinit { print("Person instance \(name) is being deallocated") } } 

Et c'est à la fin de runScenario ():

 do { let ernie = Person(name: "Ernie") let bert = Person(name: "Bert") ernie.friends.append(bert) // Not deallocated bert.friends.append(ernie) // Not deallocated } 

Lancez l'appli. Attention: ni ernie ni bert ne sont libérés.

Lien et signification


Ceci est un exemple d'une combinaison d'un type de référence et d'un type de valeur qui a conduit à une boucle de liens.

ernie et bert restent inédits, se tenant dans leurs tableaux d'amis, bien que les tableaux eux-mêmes soient des types de valeur.

Essayez de faire en sorte que les amis archivent sans propriétaire, et Xcode affichera une erreur: sans propriétaire s'applique uniquement aux classes.

Pour corriger cette boucle, nous devons créer un objet wrapper et l'utiliser pour ajouter des instances au tableau.

Ajoutez la définition suivante avant la classe Person:

 class Unowned<T: AnyObject> { unowned var value: T init (_ value: T) { self.value = value } } 

Modifiez ensuite la définition d'amis dans la classe Personne:

 var friends: [Unowned<Person>] = [] 

Enfin, remplacez le contenu du bloc do dans runScenario ():

 do { let ernie = Person(name: "Ernie") let bert = Person(name: "Bert") ernie.friends.append(Unowned(bert)) bert.friends.append(Unowned(ernie)) } 

Lancez l'application, maintenant ernie et bert sont correctement libérés!

Le tableau d'amis n'est plus une collection d'objets Person. Il s'agit maintenant d'une collection d'objets non possédés qui servent de wrappers pour les instances Person.

Pour obtenir des objets Person de Unown, utilisez la propriété value:

 let firstFriend = bert.friends.first?.value // get ernie 

Conclusion


Vous avez maintenant une bonne compréhension de la gestion de la mémoire dans Swift et vous savez comment ARC fonctionne. J'espère que la publication vous a été utile.

Apple: comptage automatique des références

Source: https://habr.com/ru/post/fr451130/


All Articles