Warum und wie können Speicherlecks im Event-Handler vermieden werden?

154

Durch das Lesen einiger Fragen und Antworten auf StackOverflow wurde mir gerade klar, dass das Hinzufügen von Ereignishandlern +=in C # (oder anderen .net-Sprachen) häufig zu Speicherlecks führen kann ...

Ich habe in der Vergangenheit viele Male solche Ereignishandler verwendet und nie bemerkt, dass sie Speicherlecks in meinen Anwendungen verursachen können oder verursacht haben.

Wie funktioniert das (was bedeutet, warum verursacht dies tatsächlich einen Speicherverlust)?
Wie kann ich dieses Problem beheben? Ist mit -=auf den gleichen Event - Handler genug?
Gibt es gängige Entwurfsmuster oder Best Practices für den Umgang mit solchen Situationen?
Beispiel: Wie soll ich mit einer Anwendung umgehen, die viele verschiedene Threads hat und viele verschiedene Ereignishandler verwendet, um mehrere Ereignisse auf der Benutzeroberfläche auszulösen?

Gibt es gute und einfache Möglichkeiten, dies in einer bereits erstellten großen Anwendung effizient zu überwachen?

gillyb
quelle

Antworten:

188

Die Ursache ist einfach zu erklären: Während ein Ereignishandler abonniert ist, enthält der Herausgeber des Ereignisses über den Ereignisbehandlungsdelegaten einen Verweis auf den Abonnenten (vorausgesetzt, der Delegat ist eine Instanzmethode).

Wenn der Herausgeber länger als der Abonnent lebt, bleibt der Abonnent auch dann am Leben, wenn keine anderen Verweise auf den Abonnenten vorliegen.

Wenn Sie das Ereignis mit einem gleichwertigen Handler abbestellen, werden der Handler und das mögliche Leck entfernt. Nach meiner Erfahrung ist dies jedoch selten ein Problem - da ich normalerweise feststelle, dass Herausgeber und Abonnent ohnehin ungefähr die gleiche Lebensdauer haben.

Es ist eine mögliche Ursache ... aber meiner Erfahrung nach ist es ziemlich übertrieben. Ihr Kilometerstand kann natürlich variieren ... Sie müssen nur vorsichtig sein.

Jon Skeet
quelle
... Ich habe einige Leute gesehen, die über Antworten auf Fragen wie "Was sind die häufigsten Speicherlecks in .net" geschrieben haben.
Gillyb
32
Eine Möglichkeit, dies von der Seite des Herausgebers aus zu umgehen, besteht darin, das Ereignis auf null zu setzen, sobald Sie sicher sind, dass Sie es nicht mehr auslösen werden. Dies entfernt implizit alle Abonnenten und kann nützlich sein, wenn bestimmte Ereignisse nur in bestimmten Phasen der Lebensdauer des Objekts ausgelöst werden.
JSB 24
2
Die Dipose-Methode wäre ein guter Moment, um das Ereignis auf null zu setzen
Davi Fiamenghi
6
@DaviFiamenghi: Nun, wenn etwas entsorgt wird, ist dies zumindest ein wahrscheinlicher Hinweis darauf, dass es bald für die Speicherbereinigung in Frage kommt. Zu diesem Zeitpunkt spielt es keine Rolle, welche Abonnenten es gibt.
Jon Skeet
Ja, es macht Sinn, sagte ich aufgrund einer Situation, die ich mit einem langlebigen Verlag mit vielen regelmäßigen Abonnenten hatte. In einem bestimmten Moment musste ich die Instanzressourcen zurücksetzen / löschen, aber die Verweise auf Abonnenten waren immer noch da, also ich Setzen Sie es auf nullon Disposeund rufen Sie es auf, um diese "nicht verwalteten" Ressourcen zu löschen. Vielleicht wäre eine bessere Möglichkeit , die Methoden werden rufen ClearSubscriptionsund ClearDataund rufen Sie Disposespäter , als ich nicht mehr den Publisher benötigen, bevor GC? Vielen Dank für die Antwort
Davi Fiamenghi
12

Ja, -=ist genug, es könnte jedoch ziemlich schwierig sein, jedes zugewiesene Ereignis jemals im Auge zu behalten. (Einzelheiten siehe Jons Beitrag). Schauen Sie sich in Bezug auf das Entwurfsmuster das schwache Ereignismuster an .

Femaref
quelle
1
msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx die 4.0-Version hat es noch.
Femaref
Wenn ich weiß, dass ein Publisher länger leben wird als der Abonnent, mache ich den Abonnenten IDisposableund melde mich von der Veranstaltung ab.
Shimmy Weitzhandler
9

Ich habe diese Verwirrung in einem Blog unter https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 erklärt . Ich werde versuchen, es hier zusammenzufassen, damit Sie eine klare Vorstellung haben.

Referenz bedeutet "Bedarf":

Zunächst müssen Sie verstehen, dass, wenn Objekt A einen Verweis auf Objekt B enthält, Objekt A Objekt B benötigt, um zu funktionieren, oder? Der Garbage Collector sammelt das Objekt B also nicht, solange das Objekt A im Speicher vorhanden ist.

Ich denke, dieser Teil sollte für einen Entwickler offensichtlich sein.

+ = Mittel, die Referenz des rechten Objekts auf das linke Objekt einfügen:

Die Verwirrung kommt jedoch vom Operator C # + =. Dieser Operator teilt dem Entwickler nicht eindeutig mit, dass die rechte Seite dieses Operators tatsächlich einen Verweis auf das Objekt auf der linken Seite einfügt.

Geben Sie hier die Bildbeschreibung ein

Und auf diese Weise, denkt das Objekt A, braucht es das Objekt B, obwohl es aus Ihrer Sicht dem Objekt A egal sein sollte, ob das Objekt B lebt oder nicht. Da das Objekt A glaubt, dass Objekt B benötigt wird, schützt Objekt A Objekt B vor dem Garbage Collector, solange Objekt A lebt. Wenn Sie jedoch nicht wollten, dass dieser Schutz dem Ereignisabonnentenobjekt gewährt wird, können Sie sagen, dass ein Speicherverlust aufgetreten ist.

Geben Sie hier die Bildbeschreibung ein

Sie können ein solches Leck vermeiden, indem Sie den Ereignishandler abnehmen.

Wie treffe ich eine Entscheidung?

Es gibt jedoch viele Ereignisse und Ereignishandler in Ihrer gesamten Codebasis. Bedeutet dies, dass Sie die Ereignishandler überall trennen müssen? Die Antwort lautet Nein. Wenn Sie dies tun müssten, wäre Ihre Codebasis mit ausführlichen Informationen wirklich hässlich.

Sie können lieber einem einfachen Flussdiagramm folgen, um festzustellen, ob ein Handler für das Trennen von Ereignissen erforderlich ist oder nicht.

Geben Sie hier die Bildbeschreibung ein

In den meisten Fällen ist das Ereignisabonnentenobjekt möglicherweise genauso wichtig wie das Ereignisverlegerobjekt, und beide sollen gleichzeitig leben.

Beispiel für ein Szenario, in dem Sie sich keine Sorgen machen müssen

Zum Beispiel ein Schaltflächenklickereignis eines Fensters.

Geben Sie hier die Bildbeschreibung ein

Hier ist der Ereignisverleger der Button und der Ereignisabonnent das MainWindow. Stellen Sie bei Anwendung dieses Flussdiagramms eine Frage: Soll das Hauptfenster (Ereignisabonnent) vor dem Button (Ereignisverleger) tot sein? Offensichtlich Nein. Richtig? Das macht nicht mal Sinn. Warum sollten Sie sich dann Sorgen machen, den Click-Event-Handler zu trennen?

Ein Beispiel, wenn eine Event-Handler-Ablösung ein MUSS ist.

Ich werde ein Beispiel geben, in dem das Abonnentenobjekt vor dem Herausgeberobjekt tot sein soll. Angenommen, Ihr MainWindow veröffentlicht ein Ereignis mit dem Namen "SomethingHappened" und Sie zeigen ein untergeordnetes Fenster aus dem Hauptfenster durch Klicken auf eine Schaltfläche an. Das untergeordnete Fenster abonniert dieses Ereignis des Hauptfensters.

Geben Sie hier die Bildbeschreibung ein

Das untergeordnete Fenster abonniert ein Ereignis des Hauptfensters.

Geben Sie hier die Bildbeschreibung ein

Anhand dieses Codes können wir klar erkennen, dass sich im Hauptfenster eine Schaltfläche befindet. Wenn Sie auf diese Schaltfläche klicken, wird ein untergeordnetes Fenster angezeigt. Das untergeordnete Fenster hört ein Ereignis aus dem Hauptfenster ab. Nachdem Sie etwas getan haben, schließt der Benutzer das untergeordnete Fenster.

Nun, gemäß dem Flussdiagramm, das ich bereitgestellt habe, wenn Sie eine Frage stellen: "Soll das untergeordnete Fenster (Ereignisabonnent) vor dem Ereignisverleger (Hauptfenster) tot sein? Die Antwort sollte JA sein. Richtig? Trennen Sie also den Ereignishandler Normalerweise mache ich das vom Unloaded-Ereignis des Fensters.

Faustregel: Wenn Ihre Ansicht (z. B. WPF, WinForm, UWP, Xamarin Form usw.) ein Ereignis eines ViewModel abonniert, denken Sie immer daran, den Ereignishandler zu trennen. Weil ein ViewModel normalerweise länger lebt als eine Ansicht. Wenn das ViewModel nicht zerstört wird, bleibt jede Ansicht, die das Ereignis dieses ViewModels abonniert hat, im Speicher, was nicht gut ist.

Proof des Konzepts mit einem Memory Profiler.

Es wird nicht viel Spaß machen, wenn wir das Konzept nicht mit einem Speicherprofiler validieren können. Ich habe in diesem Experiment den JetBrain dotMemory-Profiler verwendet.

Zuerst habe ich das MainWindow ausgeführt, das folgendermaßen angezeigt wird:

Geben Sie hier die Bildbeschreibung ein

Dann machte ich einen Erinnerungsschnappschuss. Dann habe ich dreimal auf den Button geklickt . Drei Kinderfenster tauchten auf. Ich habe alle diese untergeordneten Fenster geschlossen und im dotMemory-Profiler auf die Schaltfläche GC erzwingen geklickt, um sicherzustellen, dass der Garbage Collector aufgerufen wird. Dann machte ich einen weiteren Speicherschnappschuss und verglich ihn. Erblicken! Unsere Angst war wahr. Das untergeordnete Fenster wurde vom Garbage Collector auch nach dem Schließen nicht gesammelt. Nicht nur das, sondern auch die Anzahl der durchgesickerten Objekte für das ChildWindow-Objekt wird mit " 3 " angezeigt (ich habe dreimal auf die Schaltfläche geklickt, um 3 untergeordnete Fenster anzuzeigen ).

Geben Sie hier die Bildbeschreibung ein

Ok, dann habe ich den Ereignishandler wie unten gezeigt getrennt.

Geben Sie hier die Bildbeschreibung ein

Dann habe ich die gleichen Schritte ausgeführt und den Speicherprofiler überprüft. Diesmal wow! kein Speicherverlust mehr.

Geben Sie hier die Bildbeschreibung ein

Emran Hussain
quelle
3

Ein Ereignis ist wirklich eine verknüpfte Liste von Ereignishandlern

Wenn Sie + = neuer EventHandler für das Ereignis ausführen, spielt es keine Rolle, ob diese bestimmte Funktion zuvor als Listener hinzugefügt wurde. Sie wird einmal pro + = hinzugefügt.

Wenn das Ereignis ausgelöst wird, durchläuft es die verknüpfte Liste, Element für Element, und ruft alle Methoden (Ereignishandler) auf, die dieser Liste hinzugefügt wurden. Aus diesem Grund werden die Ereignishandler auch dann aufgerufen, wenn die Seiten nicht mehr so ​​lange ausgeführt werden, wie sie ausgeführt werden sind am Leben (verwurzelt) und sie werden am Leben sein, solange sie angeschlossen sind. Sie werden also aufgerufen, bis der Eventhandler mit einem - = neuen EventHandler ausgehängt wird.

Siehe hier

und MSDN HIER

TalentTuner
quelle