Une des plus grandes sources de MemoryLeaks dans les applications .Net (en particulier dans le cas d’applications Windows Forms, WPF et Silverlight), sont la création d’event handlers sur des events exposés par des objets ayant un temps de vie long, ou sur des events statiques (typiquement un évènement de la classe Microsoft.Win32.SystemEvents, ou un évènement exposé par un singleton – oui je sais, les puristes de la POO qui vont lire cette phrase vont vomir).
La raison est très simple : quand on crée un délégué (EventHandler ou autre), celui-ci possède une référence forte sur l’objet qui prend en charge l’évènement. De plus, quand on ajoute un délégué à un évènement (typiquement MonObjetSource.MonEvent += monEventHandler), cet évènement a une référence forte au délégué. Du coup, tant que l’on ne se désabonne pas, l’objet prenant en charge l’évènement reste référencé, et si la source de l’évènement n’est pas elle-même déréférencée (ce qui n’est jamais le cas avec des events statiques par exemple), l’objet prenant en charge l’évènement ne sera jamais collecté par le Garbage Collector : et on a un jouli potentiel memory leak (les puristes appellent ca plutôt un Handle Leak, parce que la source du problème n’est pas un oublie de désallocation de mémoire, mais un oublie de déréférencement).
Le cas typique, c’est un composant qui s’abonne à un évènement système, mais qui ne se désabonne jamais. Potentiellement à cause d’un oubli d’appel à une méthode Dispose ou quelque chose dans le genre. Un autre cas courrant, c’est le controlleur (ou le ViewModel) d’une vue qui s’abonne à un évènement d’un objet métier à longue durée de vie, et qui ne s’y désabonne pas (en Silverlight, c’est d’ailleurs très compliqué de se désabonner d’un évènement quand un contrôle sort de l’arbre visuel, car on n’a pas d’évènement UnLoad).
Comment faire face à ce problème (pas super simple à debugger pour courroner le tout, souvent incompris par les développeurs novices, et encore plus souvent détecté par le client qui se plaint d’une consommation mémoire désastreuse, à qui souvent les développeurs disent “ca peut pas venir de nous, nous on gère pas la mémoire, c’est le Garbage Collector qui sait”, et qui du coup s’en va propager la nouvelle comme quoi franchement, les Applis .Net c’est super lourd, faut 40 go de RAM pour les faire tourner, et au bout de 16 heures d’activité, ca fait systématiquement une OutOfMemoryException) ? Il existe plusieurs possibilités (liste pas forcément exhaustive) :
- Toujours, toujours, TOUJOURS se désabonner à un évènement auquel on s’est attaché, sauf pour :
- Les évènements des contrôles si et seulement si ils sont pris en charge par le UserControl ou le formulaire qui les a créés (et qui les contient)
- Si l’objet qui prend en charge l’évènement est lui même un singleton (oui je sais les puristes, le singleton c’est pas bon)
- Utiliser le WeakEventManager / WeakEvents pattern (mais c’est pas disponible sous Silverlight, et en plus c’est compliqué)
- Créer un objet WeakEventHandler qui servira de proxy entre l’évènement et sa cible (l’idée est de référencer la cible uniquement via une WeakReference).
Le problème de cette dernière solution (la seule possible dans certains cas avec Silverlight), c’est que la classe WeakEventHandler est compliquée à implémenter, et peut potentiellement cacher un nouveau leak. Je vous donne donc ici, mon implémentation de référence, ainsi qu’un code snippet permettant de générer des WeakEventHandlers spécifiques à un évènement donné. L’idée étant d’avoir au final une utilisation aussi simple que :
PropertyChangedWeakEventHandler.Initialize(this, EventSource,
(target,s, a)=>target.Messages.Add(a.PropertyName+ " changed"));
La méthode initialize prend 3 paramètres : la cible de l’évènement (qui sera encapsulée dans une WeakReference), l’objet source de l’évènement PropertyChanged, et un délégués statiques (qui n’a donc pas de référence à l’objet courrant et ne provoquera pas de leak). Ce dernier paramètre est vérifié à l’exécution en regardant si sa propriété Target est non nulle. Le délégué obtiendra une référence à l’objet cible grace à son premier paramètre.
Le code derrière cette classe est assez complexe, il repose lui même sur une classe générique abstraite que j’utilisais précédement directement, mais qui demandait un peu plus de plomberie, et était du coup beaucoup plus susceptible de provoquer des bugs.
Du coup, je vous file juste les sources contenant tout ce qu’il faut pour prendre en charge les évènements PropertyChanged, ainsi qu’un code Snippet permettant de créer vos propres WeakEventHandlers spécifiques (pour CollectionChanged, SystemEvents etc.) sans avoir à comprendre comment fonctionne la classe de base (allez, petit spoiler quand même, elle a 4 paramètres génériques, et fait largement partie des bouts de codes les moins lisibles que j’ai peu produire) : SStuff.Events.zip.