WPF ListBox 顯示圖片記憶體無法釋放問題
WPF ListBox 顯示圖片問題
作 者:WPFDevelopersOrg - 驚鏵
框架使用
.NET8
;
Visual Studio 2022
;
開發者反映了一個問題:使用
ListBox
載入多張圖片,並進行來回捲動時,發現記憶體持續增長,最終達到了
1.1GB
,並且沒有得到釋放。
xaml
程式碼如下:
<ListBoxItemsSource="{Binding ImageList}">
<ListBox.ItemTemplate>
<DataTemplate>
<ImageSource="{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 classImageDrawingVisual : FrameworkElement
{
private DrawingVisual _drawingVisual;
publicstaticreadonly DependencyProperty SourceProperty =
DependencyProperty.Register(
nameof(Source),
typeof(string),
typeof(ImageDrawingVisual),
new PropertyMetadata(null, OnSourceChanged));
publicstring Source
{
get => (string)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
publicImageDrawingVisual()
{
_drawingVisual = new DrawingVisual();
AddVisualChild(_drawingVisual);
AddLogicalChild(_drawingVisual);
}
privatestaticvoidOnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageDrawingVisual imageDrawingVisual)
{
imageDrawingVisual.LoadImage(e.NewValue asstring);
}
}
publicvoidLoadImage(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));
}
}
protectedoverrideint VisualChildrenCount => 1;
protectedoverride Visual GetVisualChild(int index)
{
if (index != 0)
{
thrownew ArgumentOutOfRangeException();
}
return _drawingVisual;
}
}
2.添加一個新的方法來清除
DrawingVisual
的圖片:
publicvoidClearImage()
{
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 classDisposableVirtualizingStackPanel : VirtualizingStackPanel
{
protectedoverridevoidOnCleanUpVirtualizedItem(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;
}
}
}
returnnull;
}
}
4.使用程式碼如下:
<ListBoxItemsSource="{Binding ImageList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<local:DisposableVirtualizingStackPanelIsVirtualizing="True"VirtualizationMode="Recycling" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<local:ImageDrawingVisual
Width="200"
Height="200"
Source="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
第二種方式最佳化後
直接從生成的目錄下執行
exe
效果看起來更加明顯
如果你對此有任何更好的想法或建議,歡迎分享。
參考資料
[1]
new BitmapImage
:
https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Image.cs,ea65ed9300b0595d