Por que e como evitar vazamentos de memória do manipulador de eventos?


154

Acabei de perceber, lendo algumas perguntas e respostas no StackOverflow, que adicionar manipuladores de eventos usando +=C # (ou eu acho, outras linguagens .net) pode causar vazamentos de memória comuns ...

Eu usei manipuladores de eventos como esse no passado muitas vezes e nunca percebi que eles podem causar, ou causaram, vazamentos de memória em meus aplicativos.

Como isso funciona (ou seja, por que isso realmente causa um vazamento de memória)?
Como posso resolver este problema ? Usar -=o mesmo manipulador de eventos é suficiente?
Existem padrões de design comuns ou práticas recomendadas para lidar com situações como essa?
Exemplo: como devo lidar com um aplicativo que possui muitos segmentos diferentes, usando muitos manipuladores de eventos diferentes para gerar vários eventos na interface do usuário?

Existem maneiras boas e simples de monitorar isso de maneira eficiente em um grande aplicativo já construído?

Respostas:


188

A causa é simples de explicar: enquanto um manipulador de eventos é inscrito, o publicador do evento mantém uma referência ao assinante por meio do delegado do manipulador de eventos (supondo que o delegado seja um método de instância).

Se o editor viver mais que o assinante, ele manterá o assinante ativo, mesmo quando não houver outras referências ao assinante.

Se você cancelar a inscrição no evento com um manipulador igual, sim, isso removerá o manipulador e o possível vazamento. No entanto, na minha experiência, isso raramente é realmente um problema - porque normalmente acho que o editor e o assinante têm uma vida útil praticamente igual.

Ela é uma possível causa ... mas na minha experiência é bastante mais sensacionalistas. Sua milhagem pode variar, é claro ... você só precisa ter cuidado.


... Eu vi algumas pessoas escrevendo sobre isso em respostas a perguntas como "qual é o vazamento de memória mais comum em .net".
gillyb

32
Uma maneira de contornar isso do lado do editor é definir o evento como nulo quando tiver certeza de que não o acionará mais. Isso removerá implicitamente todos os assinantes e pode ser útil quando determinados eventos são acionados apenas durante certos estágios da vida útil do objeto.
JSB ձոգչ

2
Dipose método seria um bom momento para definir o evento como nulo
Davi Fiamenghi

6
@DaviFiamenghi: Bem, se algo está sendo descartado, isso é pelo menos uma indicação provável de que será elegível para a coleta de lixo em breve, quando não importa quais assinantes existem.
Jon Skeet

1
@ BrainSlugs83: "e o padrão de evento típico inclui um remetente de qualquer maneira" - sim, mas esse é o produtor do evento . Normalmente, a instância do assinante do evento é relevante e o remetente não. Então, sim, se você pode se inscrever usando um método estático, isso não é um problema - mas isso raramente é uma opção na minha experiência.
Jon Skeet


9

Expliquei essa confusão em um blog em https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Vou tentar resumir aqui para que você possa ter uma ideia clara.

Meios de referência, "Necessidade":

Primeiro de tudo, você precisa entender que, se o objeto A tiver uma referência ao objeto B, isso significa que o objeto A precisa do objeto B para funcionar, certo? Portanto, o coletor de lixo não coletará o objeto B enquanto o objeto A estiver vivo na memória.

Eu acho que essa parte deve ser óbvia para um desenvolvedor.

+ = Significa, injetando referência do objeto do lado direito ao objeto da esquerda:

Mas, a confusão vem do operador C # + =. Este operador não diz claramente ao desenvolvedor que, no lado direito, está realmente injetando uma referência ao objeto do lado esquerdo.

insira a descrição da imagem aqui

E, ao fazer isso, o objeto A pensa, ele precisa do objeto B, embora, na sua perspectiva, o objeto A não deva se importar se o objeto B vive ou não. Como o objeto A pensa que o objeto B é necessário, o objeto A protege o objeto B do coletor de lixo enquanto o objeto A estiver vivo. Mas, se você não deseja que essa proteção seja dada ao objeto de assinante do evento, pode-se dizer que ocorreu um vazamento de memória.

insira a descrição da imagem aqui

Você pode evitar esse vazamento desanexando o manipulador de eventos.

Como tomar uma decisão?

Mas há muitos eventos e manipuladores de eventos em toda a sua base de código. Isso significa que você precisa desanexar os manipuladores de eventos em todos os lugares? A resposta é não. Se você tivesse que fazer isso, sua base de código será realmente feia com detalhes.

Você pode seguir um fluxograma simples para determinar se um manipulador de eventos de desanexação é necessário ou não.

insira a descrição da imagem aqui

Na maioria das vezes, você pode achar que o objeto do assinante do evento é tão importante quanto o objeto do publicador do evento e que ambos devem estar vivendo ao mesmo tempo.

Exemplo de um cenário em que você não precisa se preocupar

Por exemplo, um botão clique no evento de uma janela.

insira a descrição da imagem aqui

Aqui, o publicador do evento é o Button e o assinante do evento é a MainWindow. Ao aplicar esse fluxograma, faça uma pergunta: a Janela Principal (assinante do evento) deveria estar morta antes do Button (editor do evento)? Obviamente não. Certo? Isso nem faz sentido. Então, por que se preocupar em desanexar o manipulador de eventos click?

Um exemplo quando um desanexamento de manipulador de eventos é OBRIGATÓRIO.

Vou fornecer um exemplo em que o objeto do assinante deve estar morto antes do objeto do publicador. Digamos, sua MainWindow publica um evento chamado "SomethingHappened" e você mostra uma janela filho da janela principal com um clique no botão. A janela filho se inscreve no evento da janela principal.

insira a descrição da imagem aqui

E, a janela filho se inscreve em um evento da Janela Principal.

insira a descrição da imagem aqui

A partir desse código, podemos entender claramente que existe um botão na janela principal. Clicar nesse botão mostra uma janela filho. A janela filho ouve um evento na janela principal. Depois de fazer algo, o usuário fecha a janela filho.

Agora, de acordo com o fluxograma que forneço se você fizer uma pergunta "A janela filho (assinante do evento) deveria estar morta antes do publicador do evento (janela principal)? A resposta deveria ser SIM. Certo? Então, desconecte o manipulador de eventos Eu costumo fazer isso a partir do evento Unloaded da janela.

Uma regra prática : se sua visualização (por exemplo, WPF, WinForm, UWP, Xamarin Form etc.) se inscrever em um evento de um ViewModel, lembre-se sempre de desanexar o manipulador de eventos. Como um ViewModel geralmente dura mais que uma visualização. Portanto, se o ViewModel não for destruído, qualquer visualização do evento inscrito desse ViewModel permanecerá na memória, o que não é bom.

Prova do conceito usando um perfilador de memória.

Não será muito divertido se não pudermos validar o conceito com um criador de perfil de memória. Eu usei o profiler JetBrain dotMemory nesta experiência.

Primeiro, eu executei o MainWindow, que aparece assim:

insira a descrição da imagem aqui

Então, tirei um instantâneo de memória. Então eu cliquei no botão 3 vezes . Três janelas de crianças apareceram. Fechei todas essas janelas filhas e cliquei no botão Forçar GC no profiler dotMemory para garantir que o Garbage Collector seja chamado. Depois, peguei outro instantâneo de memória e o comparei. Ver! nosso medo era verdadeiro. A janela filho não foi coletada pelo coletor de lixo mesmo depois que elas foram fechadas. Não apenas isso, mas a contagem de objetos vazados para o objeto ChildWindow também é mostrada " 3 " (cliquei no botão 3 vezes para mostrar 3 janelas filho).

insira a descrição da imagem aqui

Ok, então, desanexei o manipulador de eventos, como mostrado abaixo.

insira a descrição da imagem aqui

Depois, executei as mesmas etapas e verifiquei o perfil de memória. Desta vez, uau! não há mais vazamento de memória.

insira a descrição da imagem aqui


3

Um evento é realmente uma lista vinculada de manipuladores de eventos

Quando você + = new EventHandler no evento, realmente não importa se essa função específica foi adicionada como ouvinte antes, ela será adicionada uma vez por + =.

Quando o evento é gerado, ele percorre a lista vinculada, item por item, e chama todos os métodos (manipuladores de eventos) adicionados a essa lista; é por isso que os manipuladores de eventos ainda são chamados, mesmo quando as páginas não estão mais em execução enquanto estão vivos (enraizados) e estarão vivos enquanto estiverem conectados. Portanto, eles serão chamados até que o manipulador de eventos seja desengatado com um - = new EventHandler.

Veja aqui

e MSDN AQUI


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.