Pourquoi et comment éviter les fuites de mémoire du gestionnaire d'événements?

154

Je viens juste de réaliser, en lisant quelques questions et réponses sur StackOverflow, que l'ajout de gestionnaires d'événements utilisant +=en C # (ou je suppose, d'autres langages .net) peut provoquer des fuites de mémoire courantes ...

J'ai utilisé de nombreux gestionnaires d'événements comme celui-ci dans le passé, et je n'ai jamais réalisé qu'ils pouvaient causer ou avoir causé des fuites de mémoire dans mes applications.

Comment cela fonctionne-t-il (c'est-à-dire pourquoi cela provoque-t-il réellement une fuite de mémoire)?
Comment puis-je résoudre ce problème? L'utilisation -=du même gestionnaire d'événements est-elle suffisante?
Existe-t-il des modèles de conception communs ou des meilleures pratiques pour gérer de telles situations?
Exemple: comment suis-je censé gérer une application qui a de nombreux threads différents, en utilisant de nombreux gestionnaires d'événements différents pour déclencher plusieurs événements sur l'interface utilisateur?

Existe-t-il des moyens simples et efficaces de surveiller cela efficacement dans une grande application déjà construite?

gillyb
la source

Réponses:

188

La cause est simple à expliquer: tandis qu'un gestionnaire d'événements est abonné, l' éditeur de l'événement détient une référence à l' abonné via le délégué du gestionnaire d'événements (en supposant que le délégué est une méthode d'instance).

Si l'éditeur vit plus longtemps que l'abonné, il gardera l'abonné en vie même s'il n'y a pas d'autres références à l'abonné.

Si vous vous désabonnez de l'événement avec un gestionnaire égal, alors oui, cela supprimera le gestionnaire et la fuite possible. Cependant, d'après mon expérience, cela pose rarement un problème - car généralement, je trouve que l'éditeur et l'abonné ont de toute façon des durées de vie à peu près égales.

Il est une cause possible ... mais dans mon expérience , il est un peu trop hype. Votre kilométrage peut varier, bien sûr ... il vous suffit d'être prudent.

Jon Skeet
la source
... J'ai vu des gens écrire à ce sujet sur des réponses à des questions telles que "quelles sont les fuites de mémoire les plus courantes dans .net".
gillyb
32
Un moyen de contourner ce problème du côté de l'éditeur consiste à définir l'événement sur null une fois que vous êtes sûr de ne plus le déclencher. Cela supprimera implicitement tous les abonnés et peut être utile lorsque certains événements ne sont déclenchés que pendant certaines étapes de la vie de l'objet.
JSB ձոգչ
2
La méthode du dipose serait un bon moment pour mettre l'événement à
zéro
6
@DaviFiamenghi: Eh bien, si quelque chose est en cours de suppression, c'est au moins une indication probable qu'il sera bientôt éligible au ramassage des ordures, à quel point peu importe le nombre d'abonnés.
Jon Skeet
1
@ BrainSlugs83: "et le modèle d'événement typique inclut de toute façon un expéditeur" - oui, mais c'est le producteur de l'événement . En règle générale, l' instance d' abonné à l'événement est pertinente, et l'expéditeur ne l'est pas. Donc oui, si vous pouvez vous abonner en utilisant une méthode statique, ce n'est pas un problème - mais c'est rarement une option dans mon expérience.
Jon Skeet
13

Oui, -=c'est assez, cependant, il pourrait être assez difficile de garder une trace de chaque événement attribué, jamais. (pour plus de détails, voir le post de Jon). Concernant le modèle de conception, regardez le modèle d'événement faible .

Femaref
la source
1
msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx la version 4.0 l'a toujours.
Femaref
Si je sais qu'un éditeur vivra plus longtemps que l'abonné, je crée l'abonné IDisposableet me désabonne de l'événement.
Shimmy Weitzhandler
10

J'ai expliqué cette confusion dans un blog à l' adresse https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Je vais essayer de le résumer ici pour que vous puissiez avoir une idée claire.

Référence signifie "Besoin":

Tout d'abord, vous devez comprendre que si l'objet A contient une référence à l'objet B, cela signifiera que l'objet A a besoin de l'objet B pour fonctionner, n'est-ce pas? Ainsi, le garbage collector ne collectera pas l'objet B tant que l'objet A est vivant dans la mémoire.

Je pense que cette partie devrait être évidente pour un développeur.

+ = Signifie, injecter la référence de l'objet côté droit à l'objet gauche:

Mais, la confusion vient de l'opérateur C # + =. Cet opérateur n'indique pas clairement au développeur que le côté droit de cet opérateur injecte en fait une référence à l'objet du côté gauche.

entrez la description de l'image ici

Et ce faisant, l'objet A pense, il a besoin de l'objet B, même si, de votre point de vue, l'objet A ne devrait pas se soucier de savoir si l'objet B vit ou non. Comme l'objet A pense que l'objet B est nécessaire, l'objet A protège l'objet B du ramasse-miettes tant que l'objet A est vivant. Mais, si vous ne vouliez pas que cette protection soit donnée à l'objet d'abonné d'événement, vous pouvez dire qu'une fuite de mémoire s'est produite.

entrez la description de l'image ici

Vous pouvez éviter une telle fuite en détachant le gestionnaire d'événements.

Comment prendre une décision?

Mais, il y a beaucoup d'événements et de gestionnaires d'événements dans toute votre base de code. Cela signifie-t-il que vous devez continuer à détacher les gestionnaires d'événements partout? La réponse est non. Si vous deviez le faire, votre base de code sera vraiment moche et verbeuse.

Vous pouvez plutôt suivre un organigramme simple pour déterminer si un gestionnaire d'événement de détachement est nécessaire ou non.

entrez la description de l'image ici

La plupart du temps, vous pouvez trouver que l'objet abonné à l'événement est aussi important que l'objet éditeur d'événement et que les deux sont censés vivre en même temps.

Exemple de scénario où vous n'avez pas à vous inquiéter

Par exemple, un événement de clic sur un bouton d'une fenêtre.

entrez la description de l'image ici

Ici, l'éditeur d'événement est le Button et l'abonné à l'événement est MainWindow. En appliquant cet organigramme, posez une question, la fenêtre principale (abonné à l'événement) est-elle censée être morte avant le bouton (éditeur de l'événement)? De toute évidence, non? Cela n'a même pas de sens. Alors, pourquoi s'inquiéter de détacher le gestionnaire d'événements de clic?

Un exemple où un détachement de gestionnaire d'événements est un MUST.

Je vais donner un exemple où l'objet abonné est censé être mort avant l'objet éditeur. Disons que votre MainWindow publie un événement nommé "SomethingHappened" et vous affichez une fenêtre enfant à partir de la fenêtre principale en cliquant sur un bouton. La fenêtre enfant s'abonne à cet événement de la fenêtre principale.

entrez la description de l'image ici

Et, la fenêtre enfant s'abonne à un événement de la fenêtre principale.

entrez la description de l'image ici

À partir de ce code, nous pouvons clairement comprendre qu'il existe un bouton dans la fenêtre principale. Cliquez sur ce bouton pour afficher une fenêtre enfant. La fenêtre enfant écoute un événement depuis la fenêtre principale. Après avoir fait quelque chose, l'utilisateur ferme la fenêtre enfant.

Maintenant, selon l'organigramme que j'ai fourni si vous posez une question "La fenêtre enfant (abonné à l'événement) est-elle censée être morte avant l'éditeur de l'événement (fenêtre principale)? La réponse devrait être OUI. D'accord? Alors, détachez le gestionnaire d'événements Je fais généralement cela à partir de l'événement Unloaded de la fenêtre.

Règle générale: si votre vue (par exemple WPF, WinForm, UWP, Xamarin Form, etc.) s'abonne à un événement d'un ViewModel, n'oubliez pas de détacher le gestionnaire d'événements. Parce qu'un ViewModel dure généralement plus longtemps qu'une vue. Ainsi, si le ViewModel n'est pas détruit, toute vue qui a souscrit l'événement de ce ViewModel restera en mémoire, ce qui n'est pas bon.

Preuve du concept à l'aide d'un profileur de mémoire.

Ce ne sera pas très amusant si nous ne pouvons pas valider le concept avec un profileur de mémoire. J'ai utilisé le profileur JetBrain dotMemory dans cette expérience.

Tout d'abord, j'ai exécuté MainWindow, qui apparaît comme ceci:

entrez la description de l'image ici

Ensuite, j'ai pris un instantané de la mémoire. Ensuite, j'ai cliqué 3 fois sur le bouton . Trois fenêtres enfants sont apparues. J'ai fermé toutes ces fenêtres enfants et cliqué sur le bouton Forcer GC dans le profileur dotMemory pour m'assurer que le garbage collector est appelé. Ensuite, j'ai pris un autre instantané de la mémoire et je l'ai comparé. Voir! notre peur était vraie. La fenêtre enfant n'a pas été collectée par le garbage collector même après leur fermeture. Non seulement cela, mais le nombre d'objets ayant subi une fuite pour l'objet ChildWindow est également affiché " 3 " (j'ai cliqué sur le bouton 3 fois pour afficher 3 fenêtres enfants).

entrez la description de l'image ici

Ok, alors, j'ai détaché le gestionnaire d'événements comme indiqué ci-dessous.

entrez la description de l'image ici

Ensuite, j'ai effectué les mêmes étapes et vérifié le profileur de mémoire. Cette fois, wow! plus de fuite de mémoire.

entrez la description de l'image ici

Emran Hussain
la source
3

Un événement est en réalité une liste chaînée de gestionnaires d'événements

Lorsque vous faites + = new EventHandler sur l'événement, peu importe si cette fonction particulière a déjà été ajoutée en tant qu'auditeur, elle sera ajoutée une fois par + =.

Lorsque l'événement est déclenché il parcourt la liste chaînée, élément par élément et appelle toutes les méthodes (gestionnaires d'événements) ajoutées à cette liste, c'est pourquoi les gestionnaires d'événements sont toujours appelés même lorsque les pages ne fonctionnent plus tant qu'elles sont vivants (enracinés), et ils seront vivants tant qu'ils seront branchés. Ils seront donc appelés jusqu'à ce que le gestionnaire d'événements soit décroché avec un - = new EventHandler.

Vois ici

et MSDN ICI

TalentTuner
la source