Acabo de darme cuenta, al leer algunas preguntas y respuestas en StackOverflow, que agregar controladores de eventos usando +=
C # (o supongo, otros lenguajes .net) puede causar pérdidas de memoria comunes ...
He usado controladores de eventos como este en el pasado muchas veces, y nunca me di cuenta de que pueden causar, o han causado, pérdidas de memoria en mis aplicaciones.
¿Cómo funciona esto (es decir, por qué esto realmente causa una pérdida de memoria)?
Como puedo solucionar este problema ? ¿Es -=
suficiente usar el mismo controlador de eventos?
¿Existen patrones de diseño comunes o mejores prácticas para manejar situaciones como esta?
Ejemplo: ¿Cómo se supone que debo manejar una aplicación que tiene muchos subprocesos diferentes, usando muchos controladores de eventos diferentes para generar varios eventos en la interfaz de usuario?
¿Hay alguna manera buena y simple de monitorear esto de manera eficiente en una gran aplicación ya construida?
Sí,
-=
es suficiente. Sin embargo, podría ser bastante difícil hacer un seguimiento de cada evento asignado, nunca. (para más detalles, ver la publicación de Jon). Con respecto al patrón de diseño, eche un vistazo al patrón de evento débil .fuente
IDisposable
y me dé de baja del evento.He explicado esta confusión en un blog en https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Trataré de resumirlo aquí para que pueda tener una idea clara.
Referencia significa "Necesidad":
En primer lugar, debe comprender que, si el objeto A tiene una referencia al objeto B, significará que el objeto A necesita el objeto B para funcionar, ¿verdad? Por lo tanto, el recolector de basura no recolectará el objeto B mientras el objeto A esté vivo en la memoria.
Creo que esta parte debería ser obvia para un desarrollador.
+ = Significa, inyectando referencia del objeto del lado derecho al objeto izquierdo:
Pero, la confusión proviene del operador C # + =. Este operador no le dice claramente al desarrollador que, el lado derecho de este operador en realidad está inyectando una referencia al objeto del lado izquierdo.
Y al hacerlo, el objeto A piensa que necesita el objeto B, aunque, desde su perspectiva, al objeto A no le importa si el objeto B vive o no. Como el objeto A cree que se necesita el objeto B, el objeto A protege al objeto B del recolector de basura mientras el objeto A esté vivo. Pero, si no desea que se brinde esa protección al objeto del suscriptor del evento, puede decir que se produjo una pérdida de memoria.
Puede evitar tal fuga separando el controlador de eventos.
¿Cómo tomar una decisión?
Pero, hay muchos eventos y controladores de eventos en toda su base de código. ¿Significa que debe seguir separando los controladores de eventos en todas partes? La respuesta es No. Si tuviera que hacerlo, su base de código será realmente fea y detallada.
Puede seguir un diagrama de flujo simple para determinar si un controlador de eventos de separación es necesario o no.
La mayoría de las veces, puede encontrar que el objeto del suscriptor del evento es tan importante como el objeto del editor del evento y se supone que ambos viven al mismo tiempo.
Ejemplo de un escenario donde no necesita preocuparse
Por ejemplo, un evento de clic de botón de una ventana.
Aquí, el editor del evento es el botón, y el suscriptor del evento es la ventana principal. Al aplicar ese diagrama de flujo, hacer una pregunta, ¿se supone que la Ventana principal (suscriptor del evento) está muerta antes que el Botón (editor del evento)? Obviamente No. ¿verdad? Eso ni siquiera tendrá sentido. Entonces, ¿por qué preocuparse por separar el controlador de eventos de clic?
Un ejemplo cuando una separación de controlador de eventos es DEBE.
Proporcionaré un ejemplo donde se supone que el objeto del suscriptor está muerto antes que el objeto del editor. Digamos, su MainWindow publica un evento llamado "SomethingHappened" y usted muestra una ventana secundaria desde la ventana principal haciendo clic en un botón. La ventana secundaria se suscribe a ese evento de la ventana principal.
Y, la ventana secundaria se suscribe a un evento de la Ventana principal.
A partir de este código, podemos entender claramente que hay un botón en la ventana principal. Al hacer clic en ese botón, se muestra una ventana secundaria. La ventana secundaria escucha un evento desde la ventana principal. Después de hacer algo, el usuario cierra la ventana secundaria.
Ahora, de acuerdo con el diagrama de flujo que proporcioné si hace una pregunta "¿Se supone que la ventana secundaria (suscriptor del evento) está muerta antes que el editor del evento (ventana principal)? La respuesta debería ser SÍ. ¿Correcto? Entonces, separe el controlador de eventos Normalmente lo hago desde el evento descargado de la ventana.
Una regla de oro: si su vista (es decir, WPF, WinForm, UWP, Xamarin Form, etc.) se suscribe a un evento de ViewModel, recuerde siempre separar el controlador de eventos. Porque un ViewModel generalmente dura más que una vista. Por lo tanto, si ViewModel no se destruye, cualquier vista que suscribió un evento de ese ViewModel permanecerá en la memoria, lo que no es bueno.
Prueba del concepto utilizando un generador de perfiles de memoria.
No será muy divertido si no podemos validar el concepto con un generador de perfiles de memoria. He usado el generador de perfiles JetBrain dotMemory en este experimento.
Primero, ejecuté MainWindow, que se muestra así:
Entonces, tomé una instantánea de memoria. Luego hice clic en el botón 3 veces . Aparecieron tres ventanas infantiles. He cerrado todas esas ventanas secundarias y he hecho clic en el botón Forzar GC en el generador de perfiles dotMemory para asegurarme de que se llama al recolector de basura. Luego, tomé otra instantánea de memoria y la comparé. ¡Mirad! Nuestro miedo era cierto. El recolector de basura no recogió la ventana infantil incluso después de que se cerraron. No solo eso, sino que el recuento de objetos filtrados para el objeto ChildWindow también se muestra " 3 " (hice clic en el botón 3 veces para mostrar 3 ventanas secundarias).
Ok, entonces separé el controlador de eventos como se muestra a continuación.
Luego, realicé los mismos pasos y verifiqué el generador de perfiles de memoria. Esta vez, ¡guau! No más pérdida de memoria.
fuente
Un evento es realmente una lista vinculada de controladores de eventos
Cuando haces + = new EventHandler en el evento, realmente no importa si esta función en particular se ha agregado antes como escucha, se agregará una vez por + =.
Cuando se genera el evento, pasa por la lista vinculada, elemento por elemento y llama a todos los métodos (controladores de eventos) agregados a esta lista, es por eso que los controladores de eventos aún se invocan incluso cuando las páginas ya no se ejecutan mientras están vivos (enraizados), y estarán vivos mientras estén conectados. Entonces serán llamados hasta que el controlador de eventos se desenganche con un - = nuevo EventHandler.
Mira aquí
y MSDN AQUÍ
fuente