[new:31/01/2011]Les events (gestion d’évènements) sont d’une grande puissance et existent dans presque tous les langages récents (et même quelques un plus anciens). Ils autorisent un modèle de programmation évènementiel qui se calque bien sur la façon dont sont gérées les IHM des OS modernes (pilotés par l’utilisateur et ses clics souris). Hélas ce concept réutilisé par le Framework .NET ne lui va pas très bien. Pire, dans un environnement managé (avec Garbage Collector) les évènements sont une source inépuisable de pertes mémoire !
Des memory leaks en managé ?
On nous aurait menti ? un environnement managé peu connaitre des pertes mémoire, comme ça, juste en programmant “normalement” et sans bug ?
Et bien oui !
Vous ne le saviez pas ? Alors il est grand temps d’envisager vos gestions d’évènement sous un autre angle et de vérifier le code que vous avez écrit jusqu’ici !
Le problème
Vous allez vite comprendre : en utilisant des évènements CLR classiques on créé, par force, des références fortes entre objets. Je m’explique : si la classe A propose l’évènement PropertyChanged par exemple (c’est-à-dire toute classe bien construite !), lorsqu’un objet B s’abonne à ce dernier, il existe une référence forte dans l’instance de A vers l’instance B. Un évènement n’est jamais qu’une gestion de callback, ce qui implique la présence d’une liste de pointeurs chez l’émetteur de l’évènement, pointeurs vers les méthodes enregistrées par tous le souscripteurs. Lorsque les conditions de l’évènement sont favorables à son apparition, la liste des abonnés est balayée et chaque méthode de chaque abonné est appelée.
Bref, Si B souscrit à l’évènement PropertyChanged de A, il existe une référence vers B stockée dans l’instance A. Ce mécanisme est automatique sous .NET ce qui fait que le programmeur s’en rend moins compte. Mais il ne s’agit rien de plus que de la pattern Observer (Gamma, Helm, Johnson, Vlissides).
Le schéma ci-dessus nous montre le jeu entre source de l’évènement (EventSource en bas) et écouteur (ou abonné, Listener en haut). L’application représentant la “glue” qui permet à EventSource et Listener d’exister ensembles dans un tout cohérent.
La source montrée sur le schéma est un ViewModel, cas classique aujourd’hui mais ce n’est qu’un simple exemple. De même, le récepteur, le Listener, est un UIElement mais ce pourrait être n’importe quoi d’autre (un UserControl, un Control...).
Que se passe-t-il si la source d’évènement a une durée de vie plus longue que celle de l’abonné ?
Dans notre exemple, supposons que l’UIElement soit supprimé de l’arbre visuel. Le ViewModel étant toujours actif. Que va-t-il se passer au niveau de la libération mémoire de l’abonné ?
... Rien. Bien qu’il semble ne plus être référencé nulle part, bien qu’il ait disparu de l’arbre visuel, il ne sera jamais effacé par le Garbage Collector. La raison ? ... Il existe toujours une référence forte vers l’abonné dans la mémoire de l’évènement exposé par la source (le ViewModel) !
Comme on le voit ci-dessus, l’application ne possède plus de référence vers le Listener, mais il existe toujours bien une telle référence dans EventSource, c'est le delegate qui pointe la méthode du Listener à appeler lorsque l’évènement se produit !
Cela est doublement fâcheux : d’abord et comme je le disais, nous avons là un cas typique de perte de mémoire puisqu’un objet qui n’est plus référencé restera malgré tout en mémoire, et puis il peut y avoir des effets de bord car à chaque fois que l’évènement se produira, la méthode du Listener continuera a être appelée... Supposons maintenant que des tas d’instances de Listener soient créées et détruites au cours de la vie du ViewModel possédant l’EventSource, on entrevoit les conséquences délétères sur la mémoire consommée par l’application ainsi même que sur sa vitesse d’exécution et donc sa réactivité (plein de code inutile continue a être appelé).
Si l’exemple utilise comme Listener un élément visuel c’est que ces objets ne proposent pas d’évènement de type Unloaded qui pourrait être attrapé pour permettre de se désabonné à tous les évènement qui étaient écoutés. Et aucune autre classe habituelle ne possède un tel évènement. Enfin rappelons que le destructeur n'est pas forcément appelé dans un environnement managé ce qui fait qu'on ne peut pas compter sur lui pour faire le ménange.
Supposons que le ViewModel en question ait une durée de vie vraiment longue (le ViewModel de la MainPage d’une application ayant une vie aussi longue que l’application elle-même par exemple), on comprend que l’entassement des pertes mémoires peut devenir énorme comme le montre le schéma suivant :
Se désabonner ?
La règle d’or, quel que soit le contexte de l’application et la méthodologie utilisée (MVVM ou non entre autre), c’est qu’il faut toujours qu’un objet se désabonne de tous les évènements qu’il écoutait avant d’être détruit (au sens le plus large, comme dans notre exemple "supprimé de l'arbre visuel" est une forme de destruction mais qui ne va pas à son terme, justement).
Dans de nombreux cas mettre en place une telle logique est simple (si les objets sont créés et détruits en des points bien connus de l’application).
Dans d’autres cela est purement impossible puisque l’objet ne sait même pas qu’il est déférencé (le déférencement étant une action d’un objet tiers, par nature).
Le désabonnement n’est donc pas aussi simple que cela à implémenter... Ce ne peut donc pas être une réponse globale au problème posé, en tout cas pas sous une forme aussi simpliste.
La solution
Même si je peux constater au quotidien que bon nombres de développeurs n’ont pas forcément conscience de ce problème, il est malgré tout connu de longue date. Et les équipes de développement du Framework autant que des produits annexes comme Silverlight, WPF ou le Toolkit sont conscientes du risque et programment d’une façon qui évite bien entendu le piège. Des évènements comme ceux supportés par les interfaces INotifyPropertyChanged (ou même INotifyCollectionChanged) sont malgré tout très souvent utilisés.
Pour régler le problème de façon radicale sans trop avoir à se poser de question ni mettre en place des usines à gaz l’équipe du ToolKit Silverlight a mis en place une parade ... imparable !
Il s’agit d’une toute petite classe, WeakEventListener, hélas ayant une visibilité “internal” ne permettant pas de la ré exploiter dans nos applications. Mais étant donnée sa taille, chacun est loisible d’en avoir une copie dans ses applications.
Les Weak References
Je renvoie le lecteur à l’un des anciens articles qui faisait justement le point sur la notion de référence faible sous .NET, un concept intégré dès le départ mais omis de la plupart des livres et des formations... Les lecteurs de Dot.Blog, gratuitement, eux connaissent le sujet depuis longtemps : Les références faibles sous .NET (weak references) (un article de 2009).
En gros, les références faibles permettent de garder un pointeur sur un objet sans que cela n’empêche le Garbage Collector de le libérer s’il n’est plus référencé ailleurs. Simple et efficace, mais cela complexifie un tout petit peu l’écriture du code, bien entendu.
En Pratique
Il suffit donc de mimer l’équipe du Toolkit dans vos applications pour vous protéger du dangereux problème présenté dans ce billet. La classe WeakEventListener fonctionne de façon très simple : c’est une instance intermédiaire entre la source de l’évènement et son abonné. Elle est référencée par la source et contient la référence vers l’abonné. Si l’abonné n’est plus référencé ailleurs, il sera supprimé. L’instance de la référence faible ne l’interdira pas. Ce fonctonnement est schématisé ici :
Quand l’objet Listener n’est plus utilisé, la mémoire ressemble à cela :
Remplacer un problème par un autre ?
Hmmm ... ceux qui ont tout suivi l’ont déjà compris, la recette n’est pas miraculeuse, elle ne fait que déplacer le problème, voire même le remplacer par exactement le même ! En effet, et le schéma ci-dessus le montre parfaitement, si le Listener peut enfin être libérer sans créer de fuite mémoire, c’est l’instance de WeakEventListener qui reste accrochée à la source !
Cela fait un peut penser à ces papiers collants qui se recollent immédiatement sur une autre partie de la main quand on secoue cette dernière pour tenter de s’en débarrasser...
Ce n’est pas faux mais il faut nuancer les choses.
Tout d’abord une instance de WeakEventListener ne pèse pas lourd et causera une fuite mémoire bien moins grave qu’un gros objet plein de variables, de code Xaml, d’animations etc...
Ensuite il n’est pas interdit pour la source de faire le ménage. Mais comment ? Il n’est pas simple de balayer la liste des abonnés d’un évènement tellement cette gestion est cachée par le Framework.
L’équipe du ToolKit aurait-elle juste lâché la proie pour l’ombre ?
La réponse : la lévitation objectivée
La réponse se trouve dans la façon de faire ce montage de références faibles. En réalité si on ne fait que mimer le système en place pour remplacer l’abonné par une référence vers l’abonné, nous venons de le voir, nous ne faisons que remplacer une fuite mémoire par une autre.
Il est clair qu’il ne faut pas implémenter la solution de façon aussi abrupte.
Il faut trouver un moyen de faire en sorte que l’objet WeakEventListener soit en “lévitation” dans le vide, il doit relier les deux intervenants (source et abonné) par des références faibles et n’être lui-même référencé par personne. Il doit “flotter” et pouvoir être libéré par le Garbarge Collector quand le Listener n’existe plus.
La mise en place est un peu délicate et repose justement sur des petites ruses d’implémentation de la classe WeakEventListener et surtout de son utilisation... Elle doit pointer des actions codées de façon statiques pour qu’aucune cible ne lui soit accrochée (méthode Target de System.Delegate,puisqu’un pointeur de méthode est un délégué).
Bref ce n’est pas si simple que ça mais une fois le concept bien compris on peut développer un code libéré de cette épée de Damoclès que sont les évènements CLR classiques...
Le Code ?
Comme je le disais il se trouve dans le Toolkit... Et comme ce dernier est fourni en code source aussi (www.codeplex.com/Silverlight) rien de plus facile que de l’extraire.
Il y a un peu plus facile en fait... Beat Kiener, un développeur suisse, s’est donné la peine d’extraire la classe, d’ajouter deux ou trois contrôles pour éviter qu’elle soit mal utilisée (ce qui ruine son effet) et d’englober tout cela dans un exemple.
Vous pouvez lire sont billet (en anglais) en complément de celui-ci (il expose plus en détail le fonctionnement de la classe, et pour illustrer mon propos je lui ai emprunté les schémas – rendre à César ce qui est sien est important) : http://blog.thekieners.com/2010/02/11/simple-weak-event-listener-for-silverlight/
Vous pouvez télécharger le code qu’il propose : code source et exemple
Conclusion
La solution du Toolkit est intéressante, les petites modifications faites par Kiener sont un plus, mais très franchement l’objet de ce billet n’est pas forcément de vous obliger à mettre tout cela en œuvre. Mon objectif était surtout de vous alerter sur un problème récurrent si ce n’est méconnu en tout cas fort peu débattu et rarement présenté. Pourtant il s’agit là d’un vrai problème qui pose question, quelque chose qui devrait être réglé par le Framework lui-même à mon avis.
Désormais vous savez que le problème existe, qu’il y a des parades (développer en connaissance de cause ou utiliser WeakEventListener),et je suis certain que vous ne regarderez plus un event de la même façon maintenant (certains vont même stresser et se replonger dans leur code pour voir si ...).
Bon, dormez bien quand même hein !
Et Stay Tuned !