【问题标题】:WPF CreateBitmapSourceFromHBitmap() memory leakWPF CreateBitmapSourceFromHBitmap() 内存泄漏
【发布时间】:2010-12-05 11:44:32
【问题描述】:

我需要逐个像素地绘制图像并将其显示在 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 内存?

【问题讨论】:

    标签: c# .net wpf memory-leaks


    【解决方案1】:

    在我的情况下,它不能直接使用这种方法。我还必须添加一个干净的垃圾收集器

        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();
        }
    

    【讨论】:

      【解决方案2】:

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

       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>
      

      【讨论】:

      • 代码与问题中的代码基本相同。根本解决不了内存泄漏问题。
      【解决方案3】:

      MSDN for Bitmap.GetHbitmap() 状态:

      备注

      您负责调用 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 声明。

      【讨论】:

      • 那行得通。测试后保留了一些剩余内存,但垃圾收集器将其拾取。谢谢朱利安。
      • 你太棒了。很长时间以来,我一直在尝试消除此错误,而您的解决方案就像一个魅力。谢谢
      【解决方案4】:

      这是一个很棒的(!!)帖子,虽然有所有的 cmets 和建议,但我花了一个小时才弄明白。因此,这里调用了使用 SafeHandles 获取 BitMapSource,然后是使用它创建 .PNG 图像文件的示例。最底部是“使用”和一些参考资料。当然,功劳都不是我的,我只是抄写员。

      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 包括: * 演示核心 * System.Core * 系统部署 * System.Drawing * 视窗基础

      【讨论】:

      • 您计算屏幕边界的代码 (Screen.AllScreens.Min/Max) 可以替换为 SystemParameters.VirtualScreenLeft/Top/Width/Height。这消除了引用 System.Windows.Forms 的需要,它不是 WPF 的一部分,并且使代码更快,尤其是当您有多个屏幕时。
      【解决方案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;
      

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

      【讨论】:

      • 不,抱歉,我无法重现您的错误。根据您的错误,我认为您尝试直接访问位图。看,发生了什么是您从 Kinect 流中复制位图并将其写入您自己的 WritableBitmap 中的所有转换代码。尝试仔细检查锁定和解锁的顺序,您在 Bitmap -> BitmapData -> WritableBitmap 之间移动,并且矩形大小合适,包括 z 轴 = bytesPerPixel。祝你好运
      【解决方案6】:

      每当处理非托管句柄时,最好使用“安全句柄”包装器:

      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 方法。

      【讨论】:

      • 非常好的迷你文章,关于我应该更了解的事情。
      • “答案”指向了正确的方向,但仍然没有用 - 我仍然内存不足 - 但你的解决方案完美无缺 - 不仅如此,我也喜欢以这种方式包装 - 它是真正的抽象和编码的未来 - 抱歉得意忘形
      猜你喜欢
      • 2015-10-14
      • 2011-02-21
      • 1970-01-01
      • 2011-03-19
      • 2011-08-12
      相关资源
      最近更新 更多