图像.RotateFlip泄漏内存 :/

6

虽然我已经编程大约11年(主要是VB6,最近6个月使用C#),但这是我第一次真正提问:)我从互联网上找到了所有答案,但这个问题我自己无法解决。您的网站是我获得最佳答案的最有帮助的地方之一!

我将展示我正在使用的代码(与相关内容的摘录)。问题是,在使用RotateFlip方法时,内存迅速增加到约200M,然后在一段时间后被GC回收。调用它的主方法每秒迭代约30次,因此性能在这里至关重要。我尝试过使用图形矩阵变换,但有时会失败并显示非翻转图像。应用程序本身基于使用网络摄像头,隐藏预览,拍摄回调图片并在picturebox中显示它。然后它从另一个类中覆盖一个矩形。这就是使用回调而不是预览窗口的原因。

Capture.cs类:

internal Bitmap LiveImage;

    int ISampleGrabberCB.BufferCB(double bufferSize, IntPtr pBuffer, int bufferLen)
    {
        LiveImage = new Bitmap(_width, _height, _stride, PixelFormat.Format24bppRgb, pBuffer);

        if (ExpImg) // local bool, used rarely when the picture saving is triggered
        {
            LiveImage.RotateFlip(RotateFlipType.RotateNoneFlipY);
            var a = LiveImage.Clone(new Rectangle(Currect.Left, Currect.Top, Currect.Width, Currect.Height),
                                    LiveImage.PixelFormat);
            using (a)
                a.Save("ocr.bmp", ImageFormat.Bmp);

        }
        else // dmnit, rotateflip leaks like h*ll but matrix transform doesn't sometimes flip :S
        {
            LiveImage.RotateFlip(RotateFlipType.RotateNoneFlipY);
            /*using (var g = Graphics.FromImage(LiveImage))
            {
                g.Transform = _mtx;
                g.DrawImage(LiveImage, 0, 0);
            }*/
        }
        GC.Collect(); // gotta use it with rotateflip, otherwise it gets crazy big, like ~200M :O
        return 0;
    }
}

在主窗体中,我有一个事件,它会更新PictureBox中的图片:
private void SetPic()
{
    pctCamera.Image = _cam.LiveImage;
    _cam.PicIsFree = false;
}

因为我需要将图像传输到另一个类中的主表单,所以我认为最合适的方法是在每个回调帧上更新的公开位图。 我不想使用矩阵变换的原因是它会减慢速度,并且有时在这种速度下无法翻转图像,而这种行为的频率因不同PC的硬件能力和CPU速度而异,即使在1.2GHz CPU的最快帧速率30fps下也经常出现此问题。
所以,你能帮我解决这个问题吗?实际上,我没有在当前版本中使用它,而是使用了被注释掉的矩阵变换,因为我觉得使用GC.Collect感觉不太好:(
谢谢!

为什么你要在每个变量中重复 LiveImage.RotateFlip(RotateFlipType.RotateNoneFlipY); 代码 - 你可以将其移到 if 块之外。 - VMAtm
你说得完全正确,实际上我没有使用它,因为我使用了矩阵,但在将代码更改为问题版本后,我忘记重新检查逻辑。谢谢! - sirWest
提供了一个答案,希望这可以帮到你。 - VMAtm
3个回答

5
pctCamera.Image = _cam.LiveImage;

你所观察到的大量内存使用是一个明显的迹象,表明你错过了调用Dispose()的机会。这让bitmap使用的非托管资源(主要是内存)可以尽早得到释放,而不是等待垃圾回收器的工作。引用的声明就是这样一种情况,你没有处理picture box引用的旧图像。修复方法:

if (pctCamera.Image != null) pctCamera.Image.Dispose();
pctCamera.Image = _cam.LiveImage;

哇,你真是个天才!这绝对有效,并且额外降低了至少4%的CPU使用率!这个问题困扰我好几个月了,你在2小时内帮助我解决了它 :) 谢谢! - sirWest
该死!我错过了最简单的一个。 - VMAtm

1
您可以这样重写您的代码:
internal Bitmap LiveImage;

int ISampleGrabberCB.BufferCB(double bufferSize, IntPtr pBuffer, int bufferLen)
{
    using (LiveImage = new Bitmap(_width, _height, _stride, PixelFormat.Format24bppRgb, pBuffer))
    {
        LiveImage.RotateFlip(RotateFlipType.RotateNoneFlipY);
        if (ExpImg) // local bool, used rarely when the picture saving is triggered
        {
            var a = LiveImage.Clone(new Rectangle(Currect.Left, Currect.Top, Currect.Width, Currect.Height),
                                    LiveImage.PixelFormat);
            using (a)
                a.Save("ocr.bmp", ImageFormat.Bmp);
        }
    }

    return 0;
}

Bitmap 是一个 Image 类,实现了 IDispose 接口。每次创建 Bitmap 时,建议使用 using 语句自动释放资源。


我刚刚将我的代码更改为您的示例。我尝试创建了一个额外的Bitmap类来进行旋转,然后将其处理掉,但是这并没有起到帮助作用。由于有额外的位图,内存使用量仍然像以前一样疯狂增长,甚至更糟。以下是我尝试过的类似方法: 从初始pBuffer创建新的位图作为内部函数Bitmap, 使用rotateflip翻转它, LiveImage = new Bitmap(tmpBMP); tmpBMP.Dispose(); 这种方法仍然会导致内存泄漏。如果我在每个回调上执行GC:Collect(),那么它会非常昂贵,但它可以保持良好的低水平,没有问题。 - sirWest
那么我认为你代码的问题不在这个模式之内,可能是pBuffer引起的。 - VMAtm
当我使用Graphics类中被注释掉的矩阵变换函数时,内存非常稳定,没有问题。所以,这不可能是问题所在。pBuffer只是指向内存的指针,没有其他作用。 - sirWest
@sirWest: 是的。内存会增加。但它不会“泄漏”。垃圾收集器只在需要时运行,使用一系列复杂的内部优化来确定最佳运行时间。这很重要,因为垃圾收集是一项昂贵的操作,肯定不是你想自己完成的操作。放弃这个:你试图解决一个不存在的问题。点击这里(https://dev59.com/lVTTa4cB1Zd3GeqPwuRL#5192350)了解更多详情。 - Cody Gray
注意:在这种情况下,问题确实是没有在需要的地方使用Dispose,但它也表明与我在应用程序中使用的其他图形函数相比,RotateFlip使用的内存要多得多。这使它看起来像是有一个泄漏,但实际上并没有。现在我们知道了:为了节省内存,使用它需要更加谨慎 :) - sirWest

-2

在这种情况下,可以使用GC.Collect。收集数据是释放它的唯一方法,在创建大型位图时也是必须的。GC.Collect会真正减慢程序的运行速度吗?

除此之外,您应该尽可能减少位图副本的数量。


1
不,GC.Collect并不是供你使用的。这是一个糟糕的建议。你只需要处理Bitmap对象,仅此而已。让垃圾回收器自行运行。你需要明白,在.NET托管环境中,垃圾回收是程序员无需干预的过程,这是最重要的事情。 - Cody Gray
其实并没有太多变化,至少我在任务管理器中看不到CPU使用率的变化。我主要是担心这么频繁地调用它,我的意思是每行代码30次,按照我的标准来说确实很多。我认为那就是正确的方法。 - sirWest
我试图在窗体代码中加载新图片后立即释放位图,例如: 'pctCamera = new Bitmap(_cam.LiveImage); _cam.LiveImage.Dispose();' 内存仍会泄漏到大约280MB,然后被回收,因此循环。我仍然认为这是使用.NET的RotateFlip时使用GDI+ API时的某个bug。API直接使用,但某些东西留下了内存垃圾。我已经修补了所有其他内存泄漏,在可以删除GC.Collect之前,这是最后一个需要解决的“漏洞”。 - sirWest
@Cody Gray 通常情况下,垃圾回收器知道自己在做什么。但是,如果创建位图并且不调用GC.Collect,则可以将它们保留在虚拟内存中,从而导致应用程序极大的减速。请记住,如果您有足够的虚拟内存,垃圾回收器通常不会被强制运行,调用GC.Collect将使内存使用量降低,并且在创建大型对象后多次调用它没有明显的负面影响。 - CodingBarfield
那个评论显示了对垃圾回收哲学的完全不理解。是的,它们可能会留在虚拟内存中,但你没有解释为什么这是一个问题。我建议在你自己理解这些问题的内部工作原理之前,避免给别人关于内存问题的建议。在这里调用Dispose是解决方案,而不是手动调用垃圾回收。 - Cody Gray
1
多年前,在只有192MB RAM的旧机器上,保留几个40MB大小的位图是非常糟糕的。调用GC.Collect而不是Bitmap.Dispose可以更快地清除内存并保持更多的空闲内存。Bitmap.Dispose函数显然没有告诉操作系统实际清除空间。这更接近于2000年而不是2010年,因此实现可能已经发生了很大变化。如果创建大量数据,则垃圾回收并不能使内存管理更容易。问题在于将大量内存交换到硬盘(虚拟内存)会产生太多开销。 - CodingBarfield

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