At DoorDash we are consistently making an effort to increase our user experience by increasing our app's stability. A major part of this effort is to prevent, fix and remove any retain cycles and memory leaks in our large codebase. In order to detect and fix these issues, we have found the Memory Graph Debugger to be quick and easy to use. After significantly increasing our OOM-free session rate on our Dasher iOS app, we would like to share some tips on avoiding and fixing retain cycles as well as a quick introduction using Xcode's memory graph debugger for those who are not familiar.
Si l'identification des causes profondes des problèmes de mémoire vous intéresse, consultez notre nouvel article de blog Examiner les problèmes de mémoire dans les applications C/C++ avec BPF, perf et Memcheck pour une explication détaillée du fonctionnement de la mémoire.
I. Que sont les cycles de rétention et les fuites de mémoire ?
A fuite de mémoire dans iOS, c'est lorsqu'un espace mémoire alloué ne peut pas être désalloué en raison de cycles de rétention. Étant donné que Swift utilise le Comptage automatique des références (ARC), a cycle de rétention se produit lorsque deux objets ou plus détiennent des références fortes l'un vers l'autre. Par conséquent, ces objets se conservent mutuellement en mémoire parce que leur compte de conservation ne sera jamais décrémenté à 0, ce qui empêchera les opérations de deinit d'être appelé et la mémoire d'être libérée.
II. Pourquoi se préoccuper des fuites de mémoire ?
Les fuites de mémoire augmentent progressivement l'empreinte mémoire de votre application et, lorsqu'elle atteint un certain seuil, le système d'exploitation (iOS) déclenche un avertissement de mémoire. Si cet avertissement n'est pas pris en compte, votre application sera tuée par la force, ce qui constitue une erreur de type OOM (Out of memory) plantage. Comme vous pouvez le constater, les fuites de mémoire peuvent être très problématiques si une fuite substantielle se produit parce qu'après avoir utilisé votre application pendant un certain temps, l'application se bloquerait.
En outre, les fuites de mémoire peuvent introduire effets secondaires dans votre application. Cela se produit généralement lorsque des observateurs sont conservés en mémoire alors qu'ils auraient dû être désalloués. Ces observateurs fuyants continueront d'écouter les notifications et, lorsqu'ils seront déclenchés, l'application sera sujette à des comportements imprévisibles ou à des plantages. Dans la section suivante, nous allons nous familiariser avec le débogueur de graphe de mémoire de Xcode et l'utiliser pour trouver des fuites de mémoire dans un exemple d'application.
III. Introduction to Xcode's Memory Graph Debugger
Pour l'ouvrir, lancez votre application (dans ce cas, je lance une application de démonstration) et tapez sur le bouton à 3 nœuds situé entre le débogueur visuel et le bouton du simulateur d'emplacement. Vous obtiendrez ainsi un instantané de l'état actuel de votre application.
Le panneau de gauche indique les objets en mémoire pour cet instantané, suivi du nombre d'instances de chaque classe à côté de son nom.
ex : (MainViewController(1))
Signifie qu'il n'y a que un MainViewController
dans la mémoire au moment de l'instantané, suivi de l'adresse de cette instance dans la mémoire en dessous.
Restez informé grâce aux mises à jour hebdomadaires
Abonnez-vous à notre blog d'ingénierie pour recevoir régulièrement des informations sur les projets les plus intéressants sur lesquels notre équipe travaille.
Please enter a valid email address.
Merci de vous être abonné !
Si vous sélectionnez un objet dans le panneau de gauche, vous verrez la chaîne de références qui maintient l'objet sélectionné en mémoire. Par exemple, en sélectionnant 0x7f85204227c0
sous MainViewController
nous montrerait un graphique comme celui-ci :
- Les gras en gras signifient qu'il y a une référence forte à l'objet qu'il désigne.
- Les lignes gris clair signifient qu'il y a une référence inconnue (faible ou forte) à l'objet pointé.
- Le fait d'appuyer sur une instance dans le panneau de gauche ne vous montrera que la chaîne de références qui maintient l'objet sélectionné en mémoire. Mais il ne vous montrera pas quelles sont les références de l'objet sélectionné.
Par exemple, pour vérifier qu'il n'y a pas de cycle de rétention dans les objets qui MainViewController
a une référence forte, vous devrez examiner votre base de code pour identifier les objets référencés, puis sélectionner individuellement chacun des graphes d'objets pour vérifier s'il y a un cycle de rétention.
En outre, le débogueur de graphe de mémoire peut détecter automatiquement les fuites de mémoire simples et vous envoyer des avertissements tels que ce violet !
marque. En tapant dessus, vous verrez apparaître les instances qui ont fait l'objet d'une fuite dans le panneau de gauche.
Please note that the Xcode's auto-detection does not always catch every memory leak, and oftentimes you will have to find them yourself. In the next section, I will explain the approach to using the memory graph debugger for debugging.
IV. Approche de l'utilisation du débogueur de graphe de mémoire
Une approche utile pour détecter les fuites de mémoire consiste à exécuter l'application à travers certains flux principaux et à prendre un instantané de la mémoire pour la première itération et les itérations suivantes.
- Exécutez un flux ou une fonctionnalité de base et quittez-la, puis répétez l'opération plusieurs fois et prenez un instantané de la mémoire de l'application. Regardez quels objets sont en mémoire et quelle quantité de chaque instance existe par objet.
- Vérifiez les signes suivants d'un cycle de rétention ou d'une fuite de mémoire :
- Dans le panneau de gauche, voyez-vous des objets, classes, vues, etc. dans la liste qui ne devraient pas s'y trouver ou qui auraient dû être désalloués ?
- Y a-t-il de plus en plus d'instances d'une même classe qui sont conservées en mémoire ? ex :
MainViewController (1)
devientMainViewController (5)
après avoir effectué 4 itérations supplémentaires ? - Regardez le navigateur de débogage sur le panneau de gauche, remarquez-vous une augmentation de la mémoire ? L'application consomme-t-elle plus de mégaoctets (Mo) qu'auparavant, bien qu'elle soit revenue à son état d'origine ?
- If you have found an instance that shouldn't be in memory anymore, you have found a leaked instance of an object.
- Tapez sur cette instance fuyante et utilisez le graphe d'objets pour retrouver l'objet qui la retient en mémoire.
- Il se peut que vous deviez continuer à naviguer dans les graphes d'objets pour trouver le nœud parent qui garde la chaîne d'objets en mémoire.
- Une fois que vous pensez avoir trouvé le nœud parent, examinez le code de cet objet, trouvez l'origine du référencement fort circulaire et corrigez-le.
In the next section, I will go through an example of common use cases of code that I've personally seen that causes retain cycles. To follow along, please download this sample project called LeakyApp.
V. Correction des fuites de mémoire à l'aide d'un exemple
Une fois que vous avez téléchargé le même projet Xcode, lancez l'application. Nous allons voir un exemple en utilisant le débogueur de graphe de mémoire.
- Once the app is running you will see three buttons. We will go through one example so tap on "Leaky Controller"
- Cela permettra de présenter le
ObservableViewController
qui n'est qu'une vue vide avec une barre de navigation. - Tapez sur l'élément de navigation arrière.
- Répétez cette opération plusieurs fois.
- Prenez maintenant un instantané de votre mémoire.
Après avoir pris un instantané de la mémoire, vous verrez quelque chose comme ceci :
Comme nous avons répété ce flux plusieurs fois, une fois que nous revenons à l'écran principal MainViewController
le contrôleur de vue observable aurait dû être désalloué s'il n'y avait pas de fuites de mémoire. Cependant, nous constatons que ObservableViewController (25)
dans le panneau de gauche, ce qui signifie que nous avons 25 instances de ce contrôleur de vue encore en mémoire ! Notez également que Xcode n'a pas reconnu qu'il s'agissait d'une fuite de mémoire !
Tapez ensuite sur ObservableViewController (25)
. Vous verrez le graphique de l'objet et il ressemblera à ceci :
Comme vous pouvez le voir, il s'agit d'un Swift closure context
, en conservant ObservableViewController
en mémoire. Cette fermeture est conservée en mémoire par __NSObserver
. Now let's go to the code and fix this leak.
Nous allons maintenant dans le fichier ObservableViewController.swift
. À première vue, nous avons un cas d'utilisation assez courant :
https://gist.github.com/chauvincent/33cf83b0894d9bb12d38166c15dd84a5
Nous enregistrons un observateur dans viewDidLoad
et en se retirant en tant qu'observateur dans deinit
. Cependant, il y a une utilisation délicate du code ici :
https://gist.github.com/chauvincent/b191414d54ba4cbb04614b1f85ac2e24
Nous passons une fonction en tant que fermeture ! Cette opération permet de capturer self
fortement par défaut. Vous pouvez vous référer au graphique de l'objet pour prouver que c'est le cas. NotificationCenter
semble conserver une forte référence à la fermeture, et la handleNotification
contient une référence forte à la fonction self
, en gardant cette UIViewController
et les objets auxquels il fait référence dans la mémoire !
Nous pouvons simplement corriger ce problème en ne passant pas une fonction en tant que fermeture et en ajoutant weak self
à la liste de capture :
Reconstruisez maintenant l'application et réexécutez ce flux plusieurs fois, puis vérifiez que l'objet a été désalloué en prenant un instantané de la mémoire.
Vous devriez voir quelque chose comme ceci où ObservableViewController
ne figure nulle part dans la liste après avoir quitté le flux !
La fuite de mémoire a été corrigée ! ? N'hésitez pas à tester les autres exemples dans le repo de LeakyApp, et à lire les commentaires. J'ai inclus des commentaires dans chaque fichier expliquant les causes de chaque cycle de rétention/fuite de mémoire.
VI. Conseils supplémentaires pour éviter les cycles de rétention
- Keep in mind that using a function as a closure keeps a strong reference by default. If you have to pass in a function as a closure and it causes a retain cycle, you can make an extension or operator overload to break strong reference. I won't be going over this topic but there are many resources online for this.
- Lorsque vous utilisez des vues qui ont des gestionnaires d'action à travers des fermetures, faites attention à ne pas référencer la vue à l'intérieur de sa propre fermeture ! Et si vous le faites, vous devez utiliser la liste de capture pour garder une référence faible à cette vue, avec la fermeture à laquelle la vue a une référence forte.
Par exemple, nous pouvons avoir une vue réutilisable comme celle-ci :
Dans l'appelant, nous avons un code de présentation comme celui-ci :
Il s'agit d'un cycle de rétention car someModalVC
's actionHandler
fait une forte référence à la someModalVC
. En attendant someModalVC
fait fortement référence à la actionHandler
Pour y remédier :
Nous devons nous assurer que la référence à someModalVC
est weak
en mettant à jour la liste de capture avec [weak someModalVC] in
pour rompre le cycle de rétention.
3. Lorsque vous déclarez des propriétés sur vos objets et que vous avez une variable qui est un type de protocole, assurez-vous d'ajouter une contrainte de classe et de la déclarer comme suit weak
si nécessaire ! En effet, le compilateur vous donnera une erreur par défaut si vous n'ajoutez pas de contrainte de classe. Bien qu'il soit assez bien connu que la classe delegate
dans le modèle de délégation est censé être weak
mais gardez à l'esprit que cette règle s'applique toujours à d'autres abstractions et modèles de conception, ou à toute variable de protocole que vous déclarez.
Par exemple, nous avons ici un modèle de swift propre :
Ici, nous avons besoin du OrdersListPresenter
's view
doit être une référence faible, sinon nous aurons une référence circulaire forte de la propriété View
-> Interacter
-> Presenter
-> View
. Cependant, lors de la mise à jour de cette propriété en weak var view: OrdersListDisplayLogic
nous obtiendrons une erreur de compilation.
Cette erreur du compilateur peut sembler décourageante à certains lorsqu'ils déclarent une variable de type protocole comme étant faible ! Mais dans ce cas, il faut corriger cette erreur en ajoutant une contrainte de classe au protocole !
Dans l'ensemble, j'ai trouvé que l'utilisation du débogueur de graphique de mémoire de Xcode était un moyen rapide et facile de trouver et de corriger les cycles de rétention et les fuites de mémoire ! J'espère que ces informations vous seront utiles et que vous garderez ces conseils à l'esprit tout au long de votre développement ! Je vous remercie !