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?
null
onDispose
und rufen Sie es auf, um diese "nicht verwalteten" Ressourcen zu löschen. Vielleicht wäre eine bessere Möglichkeit , die Methoden werden rufenClearSubscriptions
undClearData
und rufen SieDispose
später , als ich nicht mehr den Publisher benötigen, bevor GC? Vielen Dank für die AntwortJa,
-=
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 .quelle
IDisposable
und melde mich von der Veranstaltung ab.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.
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.
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.
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.
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.
Das untergeordnete Fenster abonniert ein Ereignis des Hauptfensters.
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:
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 ).
Ok, dann habe ich den Ereignishandler wie unten gezeigt getrennt.
Dann habe ich die gleichen Schritte ausgeführt und den Speicherprofiler überprüft. Diesmal wow! kein Speicherverlust mehr.
quelle
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
quelle