【问题标题】:BitmapData from LockBits consuming memory来自 LockBits 的 BitmapData 消耗内存
【发布时间】:2014-03-25 05:40:43
【问题描述】:

我有一个 C# 例程来获取 YUV422 位图并转换为 RGB:

    private unsafe void YUV422toRGB(byte[] YUV422, int YUVstride, ref Bitmap RGB)
    {
        //Found http://pastebin.com/MFsDnUCq after I wrote this.

        int row, col, index;
        byte y1, y2, u, v;
        int r1, r2, g1, g2, b1, b2;
        int c1, c2, d, e;
        byte* RGBbytes;
        int RGBindex;

        //http://bobpowell.net/lockingbits.aspx
        //It looks as though this bmp guy is consuming memory to the point
        //where I must force the garbage collector or the program will crash.
        //Why?
        System.Drawing.Imaging.BitmapData bmp =
            RGB.LockBits(
            new Rectangle(0, 0, RGB.Width, RGB.Height),
            System.Drawing.Imaging.ImageLockMode.WriteOnly,
            RGB.PixelFormat);
        RGBbytes = (byte*)bmp.Scan0;
        RGBindex = 0;
        index = 0;
        for (row = 0; row < RGB.Height; row++)
        {
            for (col = 0; col < YUVstride; col += 4)
            {
                u = YUV422[index + 0];
                y1 = YUV422[index + 1];
                v = YUV422[index + 2];
                y2 = YUV422[index + 3];
                index += 4;

                c1 = y1 - 16;
                c2 = y2 - 16;
                d = u - 128;
                e = v - 128;

                int c298 = 298 * c1;
                r1 = (c298 + 409 * e + 128) >> 8;
                g1 = (c298 - 100 * d - 208 * e + 128) >> 8;
                b1 = (c298 + 516 * d + 128) >> 8;

                c298 = 298 * c2;
                r2 = (c298 + 409 * e + 128) >> 8;
                g2 = (c298 - 100 * d - 208 * e + 128) >> 8;
                b2 = (c298 + 516 * d + 128) >> 8;

                //Now for clamping.
                //From http://graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax
                //min(x, y) = y ^ ((x ^ y) & -(x < y))
                //max(x, y) = x ^ ((x ^ y) & -(x < y))
                //We want min(x, 255) followed by max(x, 0).
                //The problem is that x < y in C# is a bool which cannot be converted to int.
                //But effectively, -(x < y) is -1 if x < y and 0 otherwise.
                //we can do this by looking at the first bit of x-y
                //min(x, y) = y ^ ((x ^ y) & ((x - y) >> 31))
                //max(x, y) = x ^ ((x ^ y) & ((x - y) >> 31))
                //There appears to be 10% or so speed increase with the bithack.

                //r1 = Math.Max(0, Math.Min(r1, 255));
                //r2 = Math.Max(0, Math.Min(r2, 255));
                //g1 = Math.Max(0, Math.Min(g1, 255));
                //g2 = Math.Max(0, Math.Min(g2, 255));
                //b1 = Math.Max(0, Math.Min(b1, 255));
                //b2 = Math.Max(0, Math.Min(b2, 255));

                r1 = 255 ^ ((r1 ^ 255) & ((r1 - 255) >> 31));
                g1 = 255 ^ ((g1 ^ 255) & ((g1 - 255) >> 31));
                b1 = 255 ^ ((b1 ^ 255) & ((b1 - 255) >> 31));
                r2 = 255 ^ ((r2 ^ 255) & ((r2 - 255) >> 31));
                g2 = 255 ^ ((g2 ^ 255) & ((g2 - 255) >> 31));
                b2 = 255 ^ ((b2 ^ 255) & ((b2 - 255) >> 31));

                r1 = r1 ^ ((r1 ^ 0) & ((r1 - 0) >> 31));
                g1 = g1 ^ ((g1 ^ 0) & ((g1 - 0) >> 31));
                b1 = b1 ^ ((b1 ^ 0) & ((b1 - 0) >> 31));
                r2 = r2 ^ ((r2 ^ 0) & ((r2 - 0) >> 31));
                g2 = g2 ^ ((g2 ^ 0) & ((g2 - 0) >> 31));
                b2 = b2 ^ ((b2 ^ 0) & ((b2 - 0) >> 31));

                RGBbytes[RGBindex + 0] = (byte)b1;
                RGBbytes[RGBindex + 1] = (byte)g1;
                RGBbytes[RGBindex + 2] = (byte)r1;

                RGBbytes[RGBindex + 3] = (byte)b2;
                RGBbytes[RGBindex + 4] = (byte)g2;
                RGBbytes[RGBindex + 5] = (byte)r2;

                RGBindex += 6;
            }
        }

        RGB.UnlockBits(bmp);
    }

在经典的“嘿,为什么我的程序总是在 30 秒后崩溃”之后,我意识到垃圾收集器跟不上(我每秒调用此函数 10 次,位图始终通过引用传递)。

单步执行代码,我看到RGB.LockBits 是增加 RAM 使用率的代码。我每十次调用(每秒一次)添加一个GC.Collect(),现在一切都很好。

我在这里做错了吗? UnlockBits 不应该自己清理吗?

【问题讨论】:

  • 我正在做非常相似的计算,但我从未观察到这种行为。你的位图有多大?
  • 你是在发布模式下调试器分离的情况下运行它吗?
  • 消耗内存是 LockBits() 的意图。程序中真正的错误是位图上缺少 Dispose() 调用。是的, GC.Collect() 掩盖了你的错误。这是大锤解决方案。
  • @ScottChamberlain:不,在调试模式下运行。
  • @HansPassant:否定。我正在重用位图,并且同一个程序具有与具有相同“位图重用”方法的不同相机的接口,并且没有内存蠕变问题。

标签: c# .net bitmap gdi+


【解决方案1】:

单步执行代码,我发现 RGB.LockBits 是增加 RAM 使用率的代码。我每十次调用(每秒一次)添加一个 GC.Collect(),现在一切都很好。

您所说的基本上是正常的垃圾收集器会回收所有东西,问题是垃圾收集器没有按您的预期执行。

您的机器内存不足吗?您是否存在会迫使垃圾收集器触发和回收空间的实际内存压力?或者您是否有数 GB 的可用内存,以至于暂停所有线程以回收内存只是浪费 CPU 资源?听起来你的机器只是耸耸肩说:“嗯,我这里有更多内存。一切照旧。”

【讨论】:

  • 我有 32 GB 的 RAM (Windows 7 x64) 但程序是 x86,所以当它达到 2 GB 时它就会死掉。
【解决方案2】:

Hans Passant 是正确的。问题是一个被复制的位图,尽管我仍然不清楚为什么这会导致垃圾收集器显然无法完成它的工作。

我在这个程序中有一些后端类,它们从相机获取图像并将位图转换为适当的格式(例如,我发布的 YUV422 到 RGB 转换)。被传递的位图被重复使用。

然后我有一些 UI 类,其中之一是一个窗口,它在以或多或少 10 Hz 运行的计时器上显示新帧。在这里您必须复制位图,因为后端的东西将用新框架等替换它。

更新显示的例程很简单:

    public void SetImage(Bitmap Image)
    {
        if (this.IsDisposed) return;
        if (this.InvokeRequired)
            this.Invoke((MethodInvoker)delegate { SetImage(Image); });
        else
            picFrame.Image = (Bitmap)Image.Clone();
    }

一个摄像头提供 640x480x8 位(黑白)图像;在后端,我还使用 lockbits 将我从 API 调用中获得的像素数组复制到 .NET 位图。即使同时运行其中两个摄像头(和显示窗口),我的内存也没有问题。

1920x1080 YUV422 图像显然使垃圾收集器“饱和”(或者你怎么称呼它),所以这个版本的显示例程解决了这个问题:

    private Bitmap DisplayImage = null;
    public void SetImage(Bitmap Image)
    {
        if (this.IsDisposed) return;
        if (this.InvokeRequired)
            this.Invoke((MethodInvoker)delegate { SetImage(Image); });
        else
        {
            if (DisplayImage != null)
                DisplayImage.Dispose();
            DisplayImage = (Bitmap)Image.Clone();
            picFrame.Image = DisplayImage;
        }
    }

让我震惊的是,用黑白相机观察内存使用情况只发现它被固定在某个较低的数字附近,而仅彩色相机就飙升至 2 GB,没有停止的迹象。我见过一些临界垃圾收集器案例,当垃圾收集器启动时,您会看到内存逐渐上升,然后突然回落。

这里的明显区别当然是即使两个黑白相机也只有 614kB x 10 fps,而彩色相机大约是 10 倍。我什至不打算开始猜测为什么前者对垃圾收集器没有问题,而后者却有。

【讨论】:

  • 在赋值前使用if (picFrame.Image != null) picFrame.Image.Dispose();比较简单。您的“前任”也有问题,您只是没有注意到它,因为垃圾收集器运行得足够频繁,足以让位图终结器为您清理。它运行“沉重”,不必要地消耗大量非托管内存。使位图更大,您将在终结器有机会之前用完非托管内存。
猜你喜欢
  • 1970-01-01
  • 2010-10-12
  • 1970-01-01
  • 2010-10-27
  • 2011-12-13
  • 2011-10-03
  • 2012-11-24
  • 2013-10-08
  • 1970-01-01
相关资源
最近更新 更多