Dans le cadre d’UrzaGatherer (http://urzagatherer.codeplex.com), j’ai découvert un problème bien embêtant avec une liste d’images en haute définition.
En effet j’ai une ListBox qui me présente les cartes d’une collection dans un WrapPanel (qui porte une jolie animation):

Or, l’utilisateur en se promenant de collections en collections va faire changer de nombreuses fois la source de la liste et va donc déclencher de nombreuses fois le chargement de ces grosses images.
Et j’ai constaté que lorsque je réaffecte le DataContext de ma liste à une nouvelle source, les anciennes données semblent ne pas être nettoyées de la mémoire.
Ainsi, UrzaGatherer peut allègrement occuper 2Go de RAM après avoir visité une dizaine de collections (chaque collection peut peser jusqu’à 300 Mo).
Je me suis donc mis en quête de la fuite.
Mais avant d’en venir à cette dernière regardons déjà comment le binding est fait et comment la liste est construite:
1 - Notre ListBox
La liste est donc une ListBox avec comme ItemPanel un WrapPanel dans lequel j’ai glissé un behavior en provenance de Blend pour faire un effet sympa d’animations:
<ListBox ItemsSource="{Binding}" x:Name="imagesList">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Bitmap, Mode=OneWay}"/>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" ItemHeight="300">
<Interactivity:Interaction.Behaviors>
<Layout:FluidMoveBehavior Duration="00:00:00.5">
</Layout:FluidMoveBehavior>
</Interactivity:Interaction.Behaviors>
</WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
On peut voir que l’ItemTemplate se branche sur la propriété Bitmap de notre source.
2 - La source de données
Cette dernière est effectivement composée d’une liste de cartes qui portent une propriété Bitmap que je construis ainsi:
public BitmapImage Bitmap
{
get
{
if (CompletePath == null)
return null;
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CreateOptions = BitmapCreateOptions.None;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.UriSource = new Uri(CompletePath);
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
}
A partir du chemin CompletePath, je produis une BitmapImage. L’objectif étant de contrôler le cache et de pouvoir créer les images dans un thread séparé pour les affecter par la suite. Toutefois, dans notre exemple, je simplifie cette étape en branchant directement le contrôle Image sur mon Bitmap sans passer par la création asynchrone.
3 – La fuite (les fuites?)
A partir de ce code relativement simple, il me suffit de brancher 3 ou 4 collections de cartes pour voir que la mémoire n’est jamais libérée.
Mes investigations m’ont permis de voir deux soucis:
- Le behavior de Blend garde des références sur les images et de ce fait bloque le Garbage Collector et donc laisse les images en mémoire
- L’utilisation d’une BitmapImage semble également laisser des traces en mémoire et malgré mes efforts je n’ai pas pu libérer ces ressources. La seule solution : faire un binding vers le chemin (CompletePath) plutôt que vers ma Bitmap.
Je me suis donc résigner à ne plus utiliser ni le behavior ni le chargement asynchrone. Toutefois pour ce dernier point, j’ai pu faire un contournement ainsi:
- Créer une variable BindPath sur mes cartes
- L’initialiser à null et mettre le binding du contrôle Image dessus
- Binder la liste (donc pour le moment aucune image ne s’affiche puisque BindPath = null)
- Faire une boucle dans une Task qui toutes les 20 millisecondes prend une carte et met sa propriété BindPath à CompletePath. De ce fait via le fait que mes cartes sont INotityPropertyChanged, la ListBox se met à jour progressivement et donne un effet progressif agréable (plutot qu’un bon vieux figeage de l’interface à l’ancienne)
Si vous aussi vous trouvez des fuites dans l’utilisation de WPF 4.0, n’hésitez pas à me le signaler 
WPF, .Net, UrzaGatherer
Optimisation