WPF CreateBitmapSourceFromHBitmap() 内存泄漏

53

我需要逐像素绘制一幅图像,并在WPF中显示它。 我尝试通过使用System.Drawing.Bitmap,然后使用CreateBitmapSourceFromHBitmap()为WPF Image控件创建BitmapSource来实现这一目标。 由于反复调用CreateBitmapSourceFromBitmap()会导致内存泄漏,因此我在某个地方存在内存泄漏,直到程序结束时其内存使用量才会下降。 如果我不调用CreateBitmapSourceFromBitmap(),则内存使用量没有明显变化。

for (int i = 0; i < 100; i++)
{
    var bmp = new System.Drawing.Bitmap(1000, 1000);
    var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
        bmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty,
        System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
    source = null;
    bmp.Dispose();
    bmp = null;
}

如何释放BitmapSource的内存?

6个回答

89

Bitmap.GetHbitmap()在MSDN中如下所述:

备注

您需要调用GDI DeleteObject方法来释放 GDI 位图对象所使用的内存。

因此,请使用以下代码:

// at class level
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);

// your code
using (System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(1000, 1000)) 
{
    IntPtr hBitmap = bmp.GetHbitmap(); 

    try 
    {
        var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
    }
    finally 
    {
        DeleteObject(hBitmap);
    }
}

我还将您的Dispose()调用替换为using语句。


可以的。测试后会有一些残留内存,但垃圾回收器会清理它。谢谢Julien。 - Mr Bell
你太棒了。我已经试图消除这个 bug 很长时间了,而你的解决方案完美地解决了它。谢谢。 - Darthchai

24

无论何时处理未经管理的句柄,使用“安全句柄”包装器可能是一个好主意:

public class SafeHBitmapHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    [SecurityCritical]
    public SafeHBitmapHandle(IntPtr preexistingHandle, bool ownsHandle)
        : base(ownsHandle)
    {
        SetHandle(preexistingHandle);
    }

    protected override bool ReleaseHandle()
    {
        return GdiNative.DeleteObject(handle) > 0;
    }
}

当您获取一个句柄时,可以这样创建一个安全句柄(理想情况下,您的API不会暴露IntPtr,而是始终返回安全句柄):

IntPtr hbitmap = bitmap.GetHbitmap();
var handle = new SafeHBitmapHandle(hbitmap , true);

然后像这样使用:

using (handle)
{
  ... Imaging.CreateBitmapSourceFromHBitmap(handle.DangerousGetHandle(), ...)
}

SafeHandle基类提供了一种自动可处理/终结器模式,你只需要覆盖ReleaseHandle方法即可。


一篇非常好的小文章,关于我应该更好地了解的某些内容。 - Cameron
2
“answer”指向了正确的方向,但仍然无法工作 - 我仍然遇到内存不足的问题 - 但是你的解决方案完美地解决了这个问题 - 而且不仅如此,我喜欢以这种方式进行封装 - 这是真正的抽象和编码的未来 - 抱歉,我有点激动。 - Demetris Leptos

5
我有同样的需求和问题(内存泄漏)。我实现了与标记为答案相同的解决方案。但是,尽管该解决方案有效,但它对性能造成了不可接受的影响。在i7上运行,我的测试应用程序看到稳定的30-40% CPU、200-400MB RAM增加,并且垃圾收集器几乎每毫秒运行一次。
由于我正在进行视频处理,我需要更好的性能。因此,我想到了以下解决方案,希望与大家分享。 可重复使用的全局对象
//set up your Bitmap and WritableBitmap as you see fit
Bitmap colorBitmap = new Bitmap(..);
WriteableBitmap colorWB = new WriteableBitmap(..);

//choose appropriate bytes as per your pixel format, I'll cheat here an just pick 4
int bytesPerPixel = 4;

//rectangles will be used to identify what bits change
Rectangle colorBitmapRectangle = new Rectangle(0, 0, colorBitmap.Width, colorBitmap.Height);
Int32Rect colorBitmapInt32Rect = new Int32Rect(0, 0, colorWB.PixelWidth, colorWB.PixelHeight);

转化代码

private void ConvertBitmapToWritableBitmap()
{
    BitmapData data = colorBitmap.LockBits(colorBitmapRectangle, ImageLockMode.WriteOnly, colorBitmap.PixelFormat);

    colorWB.WritePixels(colorBitmapInt32Rect, data.Scan0, data.Width * data.Height * bytesPerPixel, data.Stride);

    colorBitmap.UnlockBits(data); 
}

实现示例

//do stuff to your bitmap
ConvertBitmapToWritableBitmap();
Image.Source = colorWB;

结果是CPU稳定在10-13%,RAM为70-150MB,垃圾回收器只在6分钟运行中运行了两次。


1
不好意思,我无法重现您的错误。根据您的错误信息,我认为您正在尝试直接访问位图。问题在于您正在将位图从Kinect流中复制并写入自己的WritableBitmap,所有这些都在转换代码中进行。请尝试仔细检查锁定和解锁的顺序,以及在Bitmap-> BitmapData-> WritableBitmap之间移动的方式,以及矩形的大小是否正确,包括z轴= bytesPerPixel。祝你好运。 - TrickySituation

0

这是一篇非常棒的(!!)文章,虽然有很多评论和建议,但我花了一个小时才弄清楚其中的要点。所以在这里呼吁使用SafeHandles获取BitMapSource,并且提供一个使用它创建.PNG图像文件的示例。在底部是“usings”和一些参考资料。当然,所有的功劳都不是我的,我只是记录者。

private static BitmapSource CopyScreen()
{
    var left = Screen.AllScreens.Min(screen => screen.Bounds.X);
    var top = Screen.AllScreens.Min(screen => screen.Bounds.Y);
    var right = Screen.AllScreens.Max(screen => screen.Bounds.X + screen.Bounds.Width);
    var bottom = Screen.AllScreens.Max(screen => screen.Bounds.Y + screen.Bounds.Height);
    var width = right - left;
    var height = bottom - top;

    using (var screenBmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
    {
        BitmapSource bms = null;

        using (var bmpGraphics = Graphics.FromImage(screenBmp))
        {
            IntPtr hBitmap = new IntPtr();
            var handleBitmap = new SafeHBitmapHandle(hBitmap, true);

            try
            {
                bmpGraphics.CopyFromScreen(left, top, 0, 0, new System.Drawing.Size(width, height));

                hBitmap = screenBmp.GetHbitmap();

                using (handleBitmap)
                {
                    bms = Imaging.CreateBitmapSourceFromHBitmap(
                        hBitmap,
                        IntPtr.Zero,
                        Int32Rect.Empty,
                        BitmapSizeOptions.FromEmptyOptions());

                } // using

                return bms;
            }
            catch (Exception ex)
            {
                throw new ApplicationException($"Cannot CopyFromScreen. Err={ex}");
            }

        } // using bmpGraphics
    }   // using screen bitmap
} // method CopyScreen

这里是用法,还有“安全句柄”类:

private void buttonTestScreenCapture_Click(object sender, EventArgs e)
{
    try
    {
        BitmapSource bms = CopyScreen();
        BitmapFrame bmf = BitmapFrame.Create(bms);

        PngBitmapEncoder encoder = new PngBitmapEncoder();
        encoder.Frames.Add(bmf);

        string filepath = @"e:\(test)\test.png";
        using (Stream stm = File.Create(filepath))
        {
            encoder.Save(stm);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Err={ex}");
    }
}

public class SafeHBitmapHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    [System.Runtime.InteropServices.DllImport("gdi32.dll")]
    public static extern int DeleteObject(IntPtr hObject);

    [SecurityCritical]
    public SafeHBitmapHandle(IntPtr preexistingHandle, bool ownsHandle)
        : base(ownsHandle)
    {
        SetHandle(preexistingHandle);
    }

    protected override bool ReleaseHandle()
    {
        return DeleteObject(handle) > 0;
    }
}

最后,让我们来看一下我的“使用情况”:
using System;
using System.Linq;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Media.Imaging;
using System.Windows.Interop;
using System.Windows;
using System.IO;
using Microsoft.Win32.SafeHandles;
using System.Security;

引用的 DLL 包括: * PresentationCore * System.Core * System.Deployment * System.Drawing * WindowsBase

你用于计算屏幕边界的代码(Screen.AllScreens.Min/Max)可以被SystemParameters.VirtualScreenLeft/Top/Width/Height替代。这样做可以避免引用不属于WPF的System.Windows.Forms,并且在有多个屏幕时使代码更快。 - DDoSolitary

0

在我的情况下,这种方法并没有直接起作用。我还必须添加一个干净的垃圾收集器。

    using (PaintMap p = new PaintMap())
    {
        System.Drawing.Image i = p.AddLineToMap("1");
        System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(i, 8419, 5953);
        IntPtr hBitmap = bmp.GetHbitmap();

        var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        Image2.ImageSource = bitmapSource;

        DeleteObject(hBitmap);

        System.GC.Collect();
    }

-1

我有一个解决方案,适用于那些想要从内存或其他类加载图像的人。

 public static InteropBitmap Bitmap2BitmapImage(Bitmap bitmap)
        {
            try
            {
                var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());

                return (InteropBitmap)source;

            }
            catch (Exception e)
            {
                MessageBox.Show("Convertion exception: " + e.Message + "\n" +e.StackTrace);
                return null;
            }
        }

然后我使用它来设置图像的源。
CurrentImage.Source = ImageConverter.Bitmap2BitmapImage(cam.Bitmap);

图像是以下定义

<Image x:Name="CurrentImage" Margin="5" StretchDirection="Both"
                Width="{Binding Width}"
                Height="{Binding Height}">
                </Image>

代码本质上与问题中的代码相同。它根本没有解决内存泄漏问题。 - DDoSolitary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接