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?
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 .la source
IDisposable
et me désabonne de l'événement.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.
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.
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.
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.
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.
Et, la fenêtre enfant s'abonne à un événement de la fenêtre principale.
À 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:
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).
Ok, alors, j'ai détaché le gestionnaire d'événements comme indiqué ci-dessous.
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.
la source
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
la source