WPF ListBox 显示图片内存无法释放问题
WPF ListBox 显示图片问题
作 者:WPFDevelopersOrg - 驚鏵
框架使用 .NET8
;Visual Studio 2022
;
开发者反映了一个问题:使用 ListBox
加载多张图片,并进行来回滚动时,发现内存持续增长,最终达到了1.1GB
,并且没有得到释放。
xaml
代码如下:
<ListBox ItemsSource="{Binding ImageList}">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Width="200" Height="200"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
csharp
代码如下:
DataContext = this;
ImageList = new ObservableCollection<ImageSource>();
var selectedPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Images");
var files = Directory.GetFiles(selectedPath);
foreach (var item in files)
{
var bitmap = new BitmapImage();
using (var fileStream = new FileStream(item, FileMode.Open, FileAccess.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = fileStream;
bitmap.EndInit();
bitmap.Freeze();
}
ImageList.Add(bitmap);
}
Title = $"图片总数:{files.Length}";
尽管代码相对简单,只是使用 <Image Source="{Binding}" />
绑定了一个 BitmapSource
对象,但每次实例化对象都会导致内存急剧增长。
优化前
问题原因是因为 `new BitmapImage`[1] 实例内存占用过高
但 BitmapSource
提供的两个依赖属性 DecodePixelHeight
和 DecodePixelWidth
。这些属性的主要作用是在加载图像时对其进行缩放,以减少内存占用并提高性能,尤其在处理大尺寸图像时非常有用。然而,当将这两个值设置为 100
时,发现内存占用被控制在最高 91
,但同时加载的图片尺寸变小了。当将 DecodePixelHeight
和 DecodePixelWidth
设置为 100
时,用作缩略图显示。当点击图片时做查看原图操作,可以将它们恢复为默认值 0
。
bitmap.DecodePixelWidth = 100;
bitmap.DecodePixelHeight = 100;
第一种方式 优化后
内存在 90
另一种方式
再换一种方式需要新建一个自定义图片类用来替换 Image
并启用 ListBox
虚拟化并实现懒加载和卸载。
1.创建一个自定义的 ImageDrawingVisual
继承自 FrameworkElement
的自定义控件,内部使用 DrawingVisual
来绘制图片。
并添加一个 Source 用于绑定图片路径
public class ImageDrawingVisual : FrameworkElement
{
private DrawingVisual _drawingVisual;
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(
nameof(Source),
typeof(string),
typeof(ImageDrawingVisual),
new PropertyMetadata(null, OnSourceChanged));
public string Source
{
get => (string)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public ImageDrawingVisual()
{
_drawingVisual = new DrawingVisual();
AddVisualChild(_drawingVisual);
AddLogicalChild(_drawingVisual);
}
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageDrawingVisual imageDrawingVisual)
{
imageDrawingVisual.LoadImage(e.NewValue as string);
}
}
public void LoadImage(string imagePath)
{
using (var drawingContext = _drawingVisual.RenderOpen())
{
var bitmap = new BitmapImage();
using (var fileStream = new FileStream(imagePath, FileMode.Open, FileAccess.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = fileStream;
bitmap.EndInit();
bitmap.Freeze();
}
drawingContext.DrawImage(bitmap, new Rect(0, 0, Width, Height));
}
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index)
{
if (index != 0)
{
throw new ArgumentOutOfRangeException();
}
return _drawingVisual;
}
}
2.添加一个新的方法来清除 DrawingVisual
的图片:
public void ClearImage()
{
using (var drawingContext = _drawingVisual.RenderOpen())
{
drawingContext.DrawRectangle(Brushes.Transparent, null, new Rect(0, 0, Width, Height));
}
}
3.创建一个自定义 DisposableVirtualizingStackPanel
继承自 VirtualizingStackPanel
然后订阅 VirtualizingStackPanel.CleanUpVirtualizedItem
事件调用ImageDrawingVisual
的ClearImage
方法清除内容,绘制一个空的矩形。因为要清空一个 DrawingVisual
的内容时,通常需要使用DrawingVisual.RenderOpen()
方法打开一个DrawingContext
并在其中执行绘制操作。简单地打开DrawingContext
并立即关闭它通常无法清空已有的内容。因此,绘制一个透明矩形是常用的技巧,它可以确保之前的绘制内容被覆盖,从而达到清空的效果。
public class DisposableVirtualizingStackPanel : VirtualizingStackPanel
{
protected override void OnCleanUpVirtualizedItem(CleanUpVirtualizedItemEventArgs e)
{
base.OnCleanUpVirtualizedItem(e);
if (e.UIElement is ListBoxItem listBoxItem)
{
ImageDrawingVisual image = FindVisualChild<ImageDrawingVisual>(listBoxItem);
if (image != null)
{
image.ClearImage();
}
}
}
private T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child != null && child is T correctlyTyped)
{
return correctlyTyped;
}
else
{
var result = FindVisualChild<T>(child);
if (result != null)
{
return result;
}
}
}
return null;
}
}
4.使用代码如下:
<ListBox ItemsSource="{Binding ImageList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<local:DisposableVirtualizingStackPanel IsVirtualizing="True" VirtualizationMode="Recycling" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<local:ImageDrawingVisual
Width="200"
Height="200"
Source="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
第二种方式优化后
直接从生成的目录下运行 exe
效果看起来更加明显
如果你对此有任何更好的想法或建议,欢迎分享。
参考资料
new BitmapImage
: https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Image.cs,ea65ed9300b0595d