You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF图片画廊应用内存占用过高优化咨询

WPF图片画廊内存优化问题

问题概述

如何让WPF图片应用达到资源管理器、FastStone Image Viewer的内存表现,避免当前应用占用过多内存?

项目说明

这是一款具备分组功能的WPF C#图片画廊应用,使用了WPF UI视觉扩展。用户可从桌面或剪贴板粘贴图片,图片以.png格式存储。

现存问题

应用运行正常,但内存占用极高。加载本地1500张共约1.6GB的独立图片时,运行时内存占用高达7GB。

已尝试方案

  • 降低缩略图质量,效果有限
  • 通过interop调用原生方式加载缩略图,但仅在资源管理器预加载缩略图后生效,无法彻底优化
  • 暂未找到除降低画质外的可靠WPF图像优化方法

项目核心代码

ImageItem数据类

[JsonObject]
public class ImageItem
{
    [JsonProperty]
    public string Path { get; set; }
    [JsonProperty]
    public List<string> Categories { get; set; }
    [JsonIgnore]
    public BitmapSource Image { get; set; }
    [JsonIgnore]
    public DateTime CreationTime => File.GetCreationTime(Path);
}

图片集合存储

public SourceCache<ImageItem, string> Images { get; } = new SourceCache<ImageItem, string>(f => f.Path);

XAML展示代码

<ui:VirtualizingItemsControl ItemsSource="{Binding Images, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
    <ui:VirtualizingItemsControl.ItemTemplate>
        <DataTemplate>
            <ui:Image Visibility="{Binding Image, Converter={StaticResource NullableToVisibilityConverter}}" MouseDoubleClick="Open" CornerRadius="8" Source="{Binding Image, Mode=OneWay}">
                <ui:Image.ContextMenu>
                    <ContextMenu>
                        <ui:MenuItem Click="Open" Header="Open" Icon="{ui:SymbolIcon Symbol=Open16}"/>
                        <ui:MenuItem Click="Edit" Header="Edit" Icon="{ui:SymbolIcon Symbol=TableEdit16}"/>
                        <ui:MenuItem Click="CopyPath" Header="Copy path" Icon="{ui:SymbolIcon Symbol=Copy16}"/>
                        <ui:MenuItem Click="Delete" Header="Delete" Icon="{ui:SymbolIcon Symbol=Delete16}"/>
                        <ui:MenuItem Click="Remove" Loaded="InitializeMenuItem" Header="Remove" Icon="{ui:SymbolIcon Symbol=ImageProhibited20}"/>
                    </ContextMenu>
                </ui:Image.ContextMenu>
            </ui:Image>
        </DataTemplate>
    </ui:VirtualizingItemsControl.ItemTemplate>

    <ui:VirtualizingItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <c:StaggeredVirtualizingPanel Spacing="8" Columns="{Binding ColumnsCount, Mode=OneWay}"/>
        </ItemsPanelTemplate>
    </ui:VirtualizingItemsControl.ItemsPanel>
</ui:VirtualizingItemsControl>

缩略图加载方法

//Loads thumbnail
public static BitmapSource LoadOrGetImage(string path)
{
    //Ensure path is not empty
    ArgumentException.ThrowIfNullOrWhiteSpace(path);
    path = Path.GetFullPath(path);

    //If image exists we pick it from dictionary
    if (Images.ContainsKey(path))
    {
        return Images[path];
    }

    //Native thumbnail loading
    Guid iid = typeof(IShellItemImageFactory).GUID;

    int hr = NativeImageLoading.SHCreateItemFromParsingName(path, IntPtr.Zero, ref iid, out IShellItemImageFactory factory);

    //Ensure method does return result and initializing factory
    if (hr != 0 || factory == null)
    {
        Marshal.ThrowExceptionForHR(hr);
    }

    //Thumbnail size
    SIZE size = new SIZE();
    size.cx = c_lowQualityWidth;
    size.cy = c_lowQualityWidth;

    //If successed it returns HBITMAP handle, otherwise it throws an exception which is catched and then we fallback to file decoding
    try
    {
        factory.GetImage(size, SIIGBF.SIIGBF_INCACHEONLY, out IntPtr hbitmap);

        BitmapSource source = Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        source.Freeze();

        NativeImageLoading.DeleteObject(hbitmap);

        Images.TryAdd(path, source);

        return source;
    }
    catch
    {
        return DecodeImage(path);
    }
    //Utilizing object which is not managed by .NET, so we need to release it manually
    finally
    {
        Marshal.ReleaseComObject(factory);
    }
}

//Decode image from file and scale it down to thumbnail size
private static BitmapSource DecodeImage(string path)
{
    using var stream = File.OpenRead(path);
    var decoder = new PngBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);

    BitmapFrame frame = decoder.Frames[0];

    double scale = c_lowQualityWidth / (double)frame.PixelWidth;
    int targetHeight = (int)(frame.PixelHeight * scale);

    TransformedBitmap image = new TransformedBitmap(frame, new ScaleTransform(scale, scale));
    image.Freeze();
    return image;
}

可能原因

加载的所有缩略图长期驻留在内存中,未按需释放。


优化解决方案

1. 实现缩略图的按需加载与自动释放

当前ImageItem的Image属性在初始化时就加载并永久持有,导致所有缩略图常驻内存。改为延迟加载+弱引用缓存:

[JsonObject]
public class ImageItem
{
    [JsonProperty]
    public string Path { get; set; }
    [JsonProperty]
    public List<string> Categories { get; set; }
    [JsonIgnore]
    private WeakReference<BitmapSource> _cachedImage;

    [JsonIgnore]
    public BitmapSource Image
    {
        get
        {
            if (_cachedImage?.TryGetTarget(out var img) == true)
                return img;
            
            img = ImageLoader.LoadOrGetImage(Path);
            _cachedImage = new WeakReference<BitmapSource>(img);
            return img;
        }
        set => _cachedImage = new WeakReference<BitmapSource>(value);
    }

    [JsonIgnore]
    public DateTime CreationTime => File.GetCreationTime(Path);
}

同时修改全局缓存为弱引用存储:

private static readonly ConcurrentDictionary<string, WeakReference<BitmapSource>> _imageCache = new();

public static BitmapSource LoadOrGetImage(string path)
{
    path = Path.GetFullPath(path);
    if (_imageCache.TryGetValue(path, out var weakRef) && weakRef.TryGetTarget(out var img))
        return img;

    // 保留原加载逻辑...
    
    _imageCache.TryAdd(path, new WeakReference<BitmapSource>(source));
    return source;
}

2. 优化BitmapSource的像素格式与缓存选项

加载缩略图时,强制转换为更节省内存的像素格式(如PixelFormats.Bgr24),并调整缓存选项避免预加载全图:

private static BitmapSource DecodeImage(string path)
{
    using var stream = File.OpenRead(path);
    // 使用OnDemand缓存选项,仅在需要时加载像素数据
    var decoder = new PngBitmapDecoder(stream, BitmapCreateOptions.None, BitmapCacheOption.OnDemand);

    BitmapFrame frame = decoder.Frames[0];
    // 转换为低内存像素格式
    var targetFormat = frame.Format.BitsPerPixel > 24 ? PixelFormats.Bgr24 : frame.Format;
    var convertedFrame = BitmapFrame.Create(frame, BitmapCreateOptions.None, BitmapCacheOption.OnDemand);

    double scale = c_lowQualityWidth / (double)frame.PixelWidth;
    int targetHeight = (int)(frame.PixelHeight * scale);

    TransformedBitmap image = new TransformedBitmap(convertedFrame, new ScaleTransform(scale, scale));
    image.Freeze();
    return image;
}

原生加载部分补充格式转换:

BitmapSource source = Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, 
    BitmapSizeOptions.FromWidthAndHeight(size.cx, size.cy));
// 转换为Bgr24减少内存占用
if (source.Format != PixelFormats.Bgr24)
{
    source = new FormatConvertedBitmap(source, PixelFormats.Bgr24, null, 0);
}
source.Freeze();

3. 完善虚拟化容器的回收逻辑

确保自定义StaggeredVirtualizingPanel正确实现容器回收时的资源释放:

protected override void ClearContainerForItemOverride(DependencyObject element, object item)
{
    if (element is FrameworkElement fe)
    {
        if (fe.FindName("ImageControl") is ui.Image imgCtrl)
        {
            imgCtrl.Source = null;
        }
        BindingOperations.ClearAllBindings(fe);
    }
    base.ClearContainerForItemOverride(element, item);
}

4. 降低渲染开销

移除不必要的视觉装饰,或降低渲染质量:

<ui:Image ... RenderOptions.BitmapScalingMode="LowQuality">

内容的提问来源于stack exchange,提问作者KonTuk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.08 11:05:54