Une fuite de mémoire se produit dans une application iOS lorsque la mémoire qui n’est plus utilisée n’est pas correctement effacée et continue de prendre de la place. Cela peut nuire aux performances de l’application et, éventuellement, lorsque l’application manque de mémoire disponible, provoquer un crash. Afin de mieux comprendre comment les fuites de mémoire se produisent, il est important de savoir d’abord comment les applications iOS gèrent leur mémoire. Jetons un coup d’œil à la façon d’éviter les fuites de mémoire dans les fermetures rapides.

Comptage automatique des références

Dans iOS, la mémoire partagée est gérée en faisant en sorte que chaque objet garde une trace du nombre d’autres objets qui lui font référence. Une fois que ce nombre de références atteint 0, ce qui signifie qu’il n’y a plus de références à l’objet, il peut être effacé en toute sécurité de la mémoire. Comme son nom l’indique, ce comptage de référence n’est nécessaire que pour les types de référence, tandis que les types de valeurs ne nécessitent aucune gestion de la mémoire. Le comptage des références était effectué manuellement dans le passé en appelant retain sur un objet pour augmenter son nombre de références, puis en appelant release sur l’objet pour diminuer son nombre de références. Ce code était presque entièrement une plaque de chaudière, ennuyeux à écrire et sujet aux erreurs. Ainsi, comme la plupart des tâches subalternes, il est devenu automatisé grâce au comptage automatique des références (ARC) qui effectue les appels de rétention et de publication nécessaires en votre nom au moment de la compilation.

Même si ARC a principalement diminué le besoin de s’inquiéter de la gestion de la mémoire, il peut toujours créer des fuites de mémoire rapides chaque fois qu’il y a des références circulaires. Par exemple, disons que nous avons une classe Person qui a une propriété pour une apartment et que la classe Apartment a une propriété Person nommée tenant:

class Person { var name: String var age: Int init(name: String, age: Int) {…} var apartment: Apartment?}class Apartment { let unit: String init(unit: String) {…} var tenant: Person?}let person = Person(name: "Bob", age: 30)let unit4A = Apartment(unit: "4A")person.apartment = unit4Aunit4A.tenant = person

Lorsque nous créons de nouveaux Person et Apartment et que nous les affectons aux propriétés des autres, ils se référencent désormais de manière circulaire. Les références circulaires peuvent également se produire avec plus de deux objets. Dans cette situation, les Person et Apartment détiennent tous deux une référence à l’autre, donc dans ce cas, aucun des deux n’atteindra jamais un nombre de références de 0 et se gardera mutuellement en mémoire, même si aucun autre objet n’a de références à l’un ou l’autre d’entre eux. C’est ce qu’on appelle un cycle de rétention et provoque une fuite de mémoire.

Connexes: Trois caractéristiques qui seraient vraiment rapides

La façon dont ARC traite les cycles de rétention est d’avoir différents types de références: forte, faible et non possédée. Les références fortes sont le genre dont nous avons déjà parlé, et ces références augmentent le nombre de références d’un objet. Les références faibles, en revanche, tout en vous donnant toujours une référence à l’objet, n’augmentent pas son nombre de références. Donc, si nous devions prendre la propriété tenant sur Apartment et en faire une référence faible à la place, cela briserait notre cycle de rétention:

class Apartment { let unit: String init(unit: String) {…} weak var tenant: Person?}

Maintenant, notre objet Person conserve toujours son Apartment en mémoire, mais l’inverse n’est plus vrai. Ainsi, lorsque la dernière référence forte à Person aura disparu, son nombre de références tombera à 0 et il libérera son Apartment, dont le nombre de références sera également ramené à 0, et ils pourront tous les deux être correctement effacés de la mémoire.

Dans Swift, les références faibles doivent être facultatives var s car, si vous ne prenez pas la responsabilité de garder un objet en mémoire, vous ne pouvez pas garantir que l’objet ne changera pas ou ne laissera pas de mémoire. C’est là que le troisième type de référence entre en jeu. les références non possédées sont comme des références faibles, sauf qu’elles peuvent être non facultatives let s, mais elles ne doivent être utilisées que lorsque vous êtes sûr que l’objet ne doit jamais être nil. Comme les options déballées par la force, vous dites au compilateur « Ne vous inquiétez pas, j’ai ça. Faites-moi confiance. »Mais comme les références faibles, la référence non possédée ne fait rien pour garder l’objet en mémoire et si elle quitte la mémoire et que vous essayez d’y accéder, votre application se bloquera. Encore une fois, tout comme les options déballées de force.

Bien que les cycles de rétention soient faciles à voir avec deux objets pointant l’un sur l’autre, ils sont plus difficiles à voir lorsque des fermetures dans Swift sont impliquées, et c’est là que j’ai vu la plupart des cycles de rétention se produire.

Éviter les cycles de rétention Dans les fermetures

Il est important de se rappeler que les fermetures sont des types de référence dans Swift et peuvent provoquer des cycles de rétention aussi facilement, sinon plus facilement, que les classes. Pour qu’une fermeture s’exécute plus tard, elle doit conserver toutes les variables dont elle a besoin pour s’exécuter. De même que les classes, une fermeture capture les références comme fortes par défaut. Un cycle de rétention avec une fermeture ressemblerait à ceci:

class SomeObject { var aClosure = { self.doSomething() } ...}

En relation: Quand Dois-je utiliser des Blocs et des Fermetures ou des Délégués pour les rappels?

 Nouvel appel à l'action

Dans ce cas, la classe SomeObject fait fortement référence à aClosure et aClosure a également capturé self (l’instance SomeObject). C’est pourquoi Swift vous oblige toujours à ajouter self. explicitement lors d’une fermeture pour empêcher les programmeurs de capturer accidentellement self sans s’en rendre compte, et donc très probablement provoquer un cycle de rétention.

Pour avoir une variable de capture de fermeture comme faible ou non possédée, vous pouvez donner les instructions de fermeture sur la façon de capturer certaines variables:

class SomeObject { var aClosure = { in self.doSomething() delegate?.doSomethingElse() } ...}

Plus d’informations sur la syntaxe de fermeture ici.

La plupart des exemples de fermeture que j’ai vus dans des tutoriels ou des exemples semblent capturer self comme non possédé et l’appeler un jour, car capturer self comme faible le rend facultatif, ce qui peut être plus gênant à utiliser. Mais comme nous l’avons appris précédemment, c’est intrinsèquement dangereux car il y aura un crash si self n’est plus en mémoire. Ce n’est pas très différent de simplement forcer le déballage de toutes vos variables facultatives parce que vous ne voulez pas faire le travail pour les déballer en toute sécurité. À moins que vous ne puissiez être sûr que self sera là aussi longtemps que votre fermeture l’est, vous devriez essayer de la capturer faible à la place. Si vous avez besoin d’un self non optionnel à l’intérieur de votre fermeture, envisagez d’utiliser un if let ou guard let pour obtenir un self solide et non optionnel à l’intérieur de la fermeture. Parce que vous avez créé cette nouvelle référence forte à l’intérieur de la fermeture Swift, vous ne créerez pas de cycle de rétention car cette référence sera libérée à la fin de la fermeture:

var aClosure = { in if let strongSelf = self { doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf) }}

ou encore mieux:

var aClosure = { in guard let strongSelf = self else { return } doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf)}

S’auto-capturer fortement

Bien qu’il soit recommandé de capturer faiblement self dans les fermetures, ce n’est pas toujours nécessaire. Les fermetures qui ne sont pas retenues par le self peuvent le capturer fortement sans provoquer de cycle de rétention. Quelques exemples courants de ceci sont:

Travailler avec DispatchQueue s dans GCD

DispatchQueue.main.async { self.doSomething() // Not a retain cycle}

Travailler avec UIView.animate(withDuration:)

UIView.animate(withDuration: 1) { self.doSomething() // Not a retain cycle}

Le premier n’est pas un cycle de rétention puisque self ne conserve pas le singleton DispatchQueue.main. Dans le deuxième exemple, UIView.animate(withDuration:) est une méthode de classe, qui self n’a pas non plus de rôle à retenir.

Capturer self fortement dans ces situations ne provoquera pas de cycle de rétention, mais ce n’est peut-être pas ce que vous voulez. Par exemple, revenir à GCD:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { self.doSomething()}

Cette fermeture ne fonctionnera pas pendant 60 secondes supplémentaires et conservera self jusqu’à ce qu’elle le fasse. C’est peut-être le comportement que vous voulez, mais si vous vouliez que self puisse laisser de la mémoire pendant ce temps, il serait préférable de le capturer faiblement et de ne s’exécuter que si self est toujours là:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { in self?.doSomething()}

Un autre endroit intéressant où self n’aurait pas besoin d’être capturé fortement est dans les variables lazy, qui ne sont pas des fermetures, car elles seront exécutées une fois (ou jamais) puis relâchées par la suite:

lazy var fullName = { return self.firstName + " " + self.lastName }()

Cependant, si une variable lazy est une fermeture, elle devrait se capturer faiblement. Un bon exemple de cela vient du guide du langage de programmation Swift:

class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { in if let text = self.text { return "<(self.name)>(text)</(self.name)>" } else { return "<(self.name) />" } }}

C’est également un bon exemple d’instance qui est raisonnable d’utiliser une référence non possédée, car la fermeture asHTML devrait exister aussi longtemps que la HTMLElement, mais plus.

TL; DR

Lorsque vous travaillez avec des fermetures dans Swift, soyez conscient de la façon dont vous capturez les variables, en particulier self s. Si self conserve la fermeture de quelque manière que ce soit, assurez-vous de la capturer faiblement. Capturez uniquement les variables comme non possédées lorsque vous pouvez être sûr qu’elles seront en mémoire chaque fois que la fermeture est exécutée, pas seulement parce que vous ne voulez pas travailler avec un self facultatif. Cela vous aidera à prévenir les fuites de mémoire lors des fermetures rapides, ce qui améliorera les performances de l’application.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.