當前位置: 妍妍網 > 碼農

WPF ListBox 顯示圖片記憶體無法釋放問題

2024-06-19碼農

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(00, 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, nullnew Rect(00, 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