Warum und wie kann man Speicherlecks im Event-Handler vermeiden?

Warum und wie kann man Speicherlecks im Event-Handler vermeiden?

Die Ursache ist einfach zu erklären:Während ein Event-Handler abonniert ist, wird der Publisher des Ereignisses enthält einen Verweis auf den Abonnenten über den Delegaten des Event-Handlers (vorausgesetzt, der Delegate ist eine Instanzmethode).

Wenn der Herausgeber länger lebt als der Abonnent, wird er den Abonnenten am Leben erhalten, auch wenn es keine anderen Verweise auf den Abonnenten gibt.

Wenn Sie das Ereignis mit einem gleichen Handler abbestellen, ja, dann werden der Handler und das mögliche Leck entfernt. Meiner Erfahrung nach ist dies jedoch selten wirklich ein Problem - da ich normalerweise feststelle, dass der Herausgeber und der 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.


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 einmal müssen Sie verstehen, dass, wenn Objekt A einen Verweis auf Objekt B enthält, dies bedeutet, dass Objekt A Objekt B benötigt, um zu funktionieren, richtig? Der Garbage Collector sammelt also das Objekt B nicht, solange das Objekt A im Speicher aktiv ist.

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

+=Bedeutet, Referenz des rechten Objekts auf das linke Objekt einzufügen:

Aber die Verwirrung kommt vom C# +=Operator. 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 dadurch denkt das Objekt A, dass es das Objekt B braucht, obwohl es aus Ihrer Sicht dem Objekt A egal sein sollte, ob das Objekt B lebt oder nicht. Da das Objekt A denkt, dass Objekt B benötigt wird, schützt Objekt A Objekt B vor dem Garbage Collector, solange Objekt A am Leben ist. Aber wenn Sie nicht wollten, dass dieser Schutz dem Ereignis-Subscriber-Objekt gegeben wird, dann können Sie sagen, dass ein Speicherleck aufgetreten ist.

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

Wie treffe ich eine Entscheidung?

Aber es gibt viele Ereignisse und Ereignishandler in Ihrer gesamten Codebasis. Bedeutet das, dass Sie überall Event-Handler trennen müssen? Die Antwort ist Nein. Wenn Sie dies tun müssten, wird Ihre Codebasis mit Ausführlichkeit wirklich hässlich sein.

Sie können eher einem einfachen Flussdiagramm folgen, um zu bestimmen, ob ein Detaching-Event-Handler notwendig ist oder nicht.

Meistens stellen Sie möglicherweise fest, dass das Event-Subscriber-Objekt genauso wichtig ist wie das Event-Publisher-Objekt, und beide sollten gleichzeitig leben.

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

Zum Beispiel ein Schaltflächenklick-Ereignis eines Fensters.

Hier ist der Ereignispublisher der Button und der Ereignisabonnent das MainWindow. Wenn Sie dieses Flussdiagramm anwenden, stellen Sie eine Frage:Soll das Hauptfenster (Ereignisabonnent) vor dem Button (Ereignisherausgeber) tot sein? Offensichtlich nein. Richtig? Das wird nicht einmal Sinn machen. Warum sich dann Gedanken über das Trennen des Click-Event-Handlers machen?

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

Ich werde ein Beispiel geben, bei dem das Subscriber-Objekt vor dem Publisher-Objekt 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.

Und das untergeordnete Fenster abonniert ein Ereignis des Hauptfensters.

Aus diesem Code können wir klar erkennen, dass es im Hauptfenster eine Schaltfläche gibt. Wenn Sie auf diese Schaltfläche klicken, wird ein untergeordnetes Fenster angezeigt. Das untergeordnete Fenster hört auf ein Ereignis aus dem Hauptfenster. Nachdem er etwas getan hat, schließt der Benutzer das untergeordnete Fenster.

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

Faustregel: Wenn Ihre Ansicht (d. h. WPF, WinForm, UWP, Xamarin Form usw.) ein Ereignis eines ViewModel abonniert, denken Sie immer daran, den Ereignishandler zu trennen. Denn ein ViewModel lebt in der Regel länger als ein View. Wenn also das ViewModel nicht zerstört wird, bleibt jede Ansicht, die das Ereignis dieses ViewModel abonniert hat, im Speicher, was nicht gut ist.

Beweis des Konzepts mit einem Speicherprofiler.

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

Zuerst habe ich das MainWindow ausgeführt, das wie folgt angezeigt wird:

Dann habe ich einen Erinnerungsschnappschuss gemacht. Dann habe ich dreimal auf die Schaltfläche geklickt . Drei untergeordnete Fenster wurden angezeigt. Ich habe alle diese untergeordneten Fenster geschlossen und im dotMemory-Profiler auf die Schaltfläche Force GC geklickt, um sicherzustellen, dass der Garbage Collector aufgerufen wird. Dann habe ich nochmal einen Erinnerungsschnappschuss gemacht und verglichen. Erblicken! unsere Befürchtung war wahr. Das untergeordnete Fenster wurde vom Garbage Collector auch nach dem Schließen nicht eingesammelt. Darüber hinaus wird die Anzahl der durchgesickerten Objekte für das ChildWindow-Objekt auch als "3" angezeigt " (Ich habe dreimal auf die Schaltfläche geklickt, um drei untergeordnete Fenster anzuzeigen).

Ok, dann habe ich den Event-Handler wie unten gezeigt getrennt.

Dann habe ich die gleichen Schritte ausgeführt und den Speicherprofiler überprüft. Dieses Mal, wow! kein Speicherleck mehr.


Ja, -= ausreicht. Es kann jedoch ziemlich schwierig sein, den Überblick über jedes zugewiesene Ereignis zu behalten. (Einzelheiten siehe Jons Beitrag). Sehen Sie sich zum Entwurfsmuster das schwache Ereignismuster an.