Perché e come evitare perdite di memoria del gestore eventi?


154

Ho appena capito, leggendo alcune domande e risposte su StackOverflow, che l'aggiunta di gestori di eventi che utilizzano +=in C # (o immagino, altri linguaggi .net) può causare perdite di memoria comuni ...

Ho usato molte volte gestori di eventi come questo in passato e non mi sono mai reso conto che possono causare o hanno causato perdite di memoria nelle mie applicazioni.

Come funziona (ovvero perché causa effettivamente una perdita di memoria)?
Come posso risolvere questo problema? È -=sufficiente utilizzare lo stesso gestore di eventi?
Esistono modelli di progettazione o best practice comuni per gestire situazioni come questa?
Esempio: come dovrei gestire un'applicazione che ha molti thread diversi, usando molti gestori di eventi diversi per generare diversi eventi sull'interfaccia utente?

Esistono modi validi e semplici per monitorarlo in modo efficiente in un'applicazione di grandi dimensioni già integrata?

Risposte:


188

La causa è semplice da spiegare: mentre un gestore di eventi è sottoscritto, l' editore dell'evento contiene un riferimento al sottoscrittore tramite il delegato del gestore di eventi (supponendo che il delegato sia un metodo di istanza).

Se l'editore vive più a lungo dell'abbonato, manterrà vivo l'abbonato anche quando non vi sono altri riferimenti all'abbonato.

Se annulli l'iscrizione all'evento con un gestore uguale, allora sì, ciò rimuoverà il gestore e l'eventuale perdita. Tuttavia, nella mia esperienza questo raramente in realtà è un problema, perché in genere trovo che l'editore e l'abbonato abbiano comunque una durata approssimativamente uguale.

Si tratta di una possibile causa ... ma nella mia esperienza è piuttosto troppo pubblicizzate. Il tuo chilometraggio può variare, ovviamente ... devi solo stare attento.


... Ho visto alcune persone scrivere di questo su risposte a domande come "qual è la perdita di memoria più comune in .net".
gillyb,

32
Un modo per aggirare questo problema dal lato del publisher è impostare l'evento su null una volta che sei sicuro di non attivarlo più. Ciò rimuoverà implicitamente tutti gli abbonati e può essere utile quando determinati eventi vengono attivati ​​solo durante determinate fasi della vita dell'oggetto.
JSB

2
Il metodo Dipose sarebbe un buon momento per impostare l'evento su null
Davi Fiamenghi,

6
@DaviFiamenghi: Beh, se qualcosa viene smaltito, questa è almeno un'indicazione probabile che sarà presto ammissibile per la raccolta dei rifiuti, a quel punto non importa quali abbonati ci siano.
Jon Skeet,

1
@ BrainSlugs83: "e lo schema di eventi tipico include comunque un mittente" - sì, ma è il produttore dell'evento . In genere l' istanza dell'abbonato all'evento è rilevante e il mittente no. Quindi sì, se puoi iscriverti usando un metodo statico, questo non è un problema, ma raramente è un'opzione nella mia esperienza.
Jon Skeet,

13

Sì, -=è abbastanza, tuttavia, potrebbe essere abbastanza difficile tenere traccia di ogni evento assegnato, mai. (per i dettagli, vedi il post di Jon). Per quanto riguarda il modello di progettazione, dai un'occhiata al modello di eventi deboli .



Se so che un editore vivrà più a lungo dell'abbonato, faccio l'abbonamento IDisposablee annullo l'iscrizione all'evento.
Shimmy Weitzhandler,

9

Ho spiegato questa confusione in un blog su https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Cercherò di riassumere qui in modo che tu possa avere un'idea chiara.

Riferimento significa "Bisogno":

Prima di tutto, devi capire che, se l'oggetto A contiene un riferimento all'oggetto B, allora, significa che l'oggetto A ha bisogno dell'oggetto B per funzionare, giusto? Pertanto, il Garbage Collector non raccoglierà l'oggetto B finché l'oggetto A è vivo nella memoria.

Penso che questa parte dovrebbe essere ovvia per uno sviluppatore.

+ = Mezzi, iniettando riferimento dell'oggetto lato destro all'oggetto sinistro:

Ma la confusione viene dall'operatore C # + =. Questo operatore non dice chiaramente allo sviluppatore che, il lato destro di questo operatore sta effettivamente iniettando un riferimento all'oggetto lato sinistro.

inserisci qui la descrizione dell'immagine

E così facendo, l'oggetto A pensa, ha bisogno dell'oggetto B, anche se, dalla tua prospettiva, all'oggetto A non dovrebbe importare se l'oggetto B vive o no. Poiché l'oggetto A ritiene che sia necessario l'oggetto B, l'oggetto A protegge l'oggetto B dal cestino della spazzatura fintanto che l'oggetto A è vivo. Ma, se non si desidera che venga data la protezione all'oggetto dell'abbonato all'evento, si può dire che si è verificata una perdita di memoria.

inserisci qui la descrizione dell'immagine

È possibile evitare tale perdita staccando il gestore eventi.

Come prendere una decisione?

Ma ci sono molti eventi e gestori di eventi nell'intera base di codice. Significa che devi continuare a staccare i gestori di eventi ovunque? La risposta è No. Se dovessi farlo, la tua base di codice sarà davvero brutta con i dettagli.

Puoi piuttosto seguire un semplice diagramma di flusso per determinare se un gestore di eventi distaccante è necessario o meno.

inserisci qui la descrizione dell'immagine

La maggior parte delle volte, potresti scoprire che l'oggetto dell'abbonato all'evento è importante quanto l'oggetto del publisher dell'evento ed entrambi dovrebbero vivere allo stesso tempo.

Esempio di uno scenario in cui non devi preoccuparti

Ad esempio, un evento clic pulsante di una finestra.

inserisci qui la descrizione dell'immagine

Qui, l'editore dell'evento è il pulsante e l'abbonato all'evento è la finestra principale. Applicando quel diagramma di flusso, ponendo una domanda, la finestra principale (abbonato all'evento) dovrebbe essere morta prima del pulsante (editore dell'evento)? Ovviamente no, vero? Non ha nemmeno senso. Quindi, perché preoccuparsi di staccare il gestore eventi click?

Un esempio in cui il distacco di un gestore di eventi è DEVE.

Fornirò un esempio in cui l'oggetto sottoscrittore dovrebbe essere morto prima dell'oggetto editore. Supponiamo che la tua MainWindow pubblichi un evento chiamato "SomethingHappened" e mostri una finestra figlio dalla finestra principale facendo clic su un pulsante. La finestra figlio si iscrive a quell'evento della finestra principale.

inserisci qui la descrizione dell'immagine

E la finestra figlio si iscrive a un evento della finestra principale.

inserisci qui la descrizione dell'immagine

Da questo codice, possiamo capire chiaramente che c'è un pulsante nella finestra principale. Facendo clic su quel pulsante viene visualizzata una finestra figlio. La finestra figlio ascolta un evento dalla finestra principale. Dopo aver fatto qualcosa, l'utente chiude la finestra figlio.

Ora, secondo il diagramma di flusso che ho fornito se fai una domanda "La finestra figlio (abbonato all'evento) dovrebbe essere morta prima che l'editore dell'evento (finestra principale)? La risposta dovrebbe essere SÌ. Giusto? Quindi, staccare il gestore dell'evento Di solito lo faccio dall'evento Unloaded della finestra.

Una regola empirica: se la tua vista (ad es. WPF, WinForm, UWP, Xamarin Form, ecc.) Si iscrive a un evento di un ViewModel, ricordati sempre di staccare il gestore dell'evento. Perché un ViewModel di solito è più lungo di una vista. Quindi, se ViewModel non viene distrutto, qualsiasi vista dell'evento sottoscritto di quel ViewModel rimarrà in memoria, il che non va bene.

Prova del concetto usando un profiler di memoria.

Non sarà molto divertente se non possiamo convalidare il concetto con un profiler di memoria. Ho usato il profiler JetBrain dotMemory in questo esperimento.

Innanzitutto, ho eseguito MainWindow, che si presenta in questo modo:

inserisci qui la descrizione dell'immagine

Quindi, ho preso un'istantanea della memoria. Quindi ho fatto clic sul pulsante 3 volte . Sono apparse tre finestre per bambini. Ho chiuso tutte quelle finestre secondarie e ho fatto clic sul pulsante Force GC nel profiler dotMemory per assicurarmi che venga chiamato Garbage Collector. Quindi, ho preso un'altra istantanea di memoria e l'ho confrontata. Ecco! la nostra paura era vera. La finestra figlio non è stata raccolta dal Garbage Collector anche dopo che sono stati chiusi. Non solo, ma anche il conteggio degli oggetti trapelati per l'oggetto ChildWindow viene mostrato " 3 " (ho fatto clic sul pulsante 3 volte per mostrare 3 finestre secondarie ).

inserisci qui la descrizione dell'immagine

Ok, quindi, ho rimosso il gestore eventi come mostrato di seguito.

inserisci qui la descrizione dell'immagine

Quindi, ho eseguito gli stessi passaggi e verificato il profiler della memoria. Questa volta, wow! non più perdite di memoria.

inserisci qui la descrizione dell'immagine


3

Un evento è in realtà un elenco collegato di gestori di eventi

Quando fai + = nuovo EventHandler sull'evento, non importa se questa particolare funzione è stata aggiunta come listener in precedenza, verrà aggiunta una volta per + =.

Quando l'evento viene generato, passa attraverso l'elenco collegato, elemento per elemento e chiama tutti i metodi (gestori di eventi) aggiunti a questo elenco, ecco perché i gestori di eventi vengono comunque chiamati anche quando le pagine non sono più in esecuzione finché sono vivi (radicati) e rimarranno vivi finché saranno collegati. Quindi verranno chiamati fino a quando il gestore degli eventi viene sganciato con un - = nuovo EventHandler.

Vedere qui

e MSDN QUI


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.