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.