垃圾回收后立即尝试保存 PictureBox.Image 时出现 AccessViolationException

Posted

技术标签:

【中文标题】垃圾回收后立即尝试保存 PictureBox.Image 时出现 AccessViolationException【英文标题】:AccessViolationException when trying to save PictureBox.Image immediately after garbage collection 【发布时间】:2018-04-23 04:33:36 【问题描述】:

我们有一个应用程序,其中包含一个与相机接口的表单。用户可以从表单控制相机,用它拍照,在 WinForms PictureBox 中显示它,打开另一个表单来编辑正在显示的图片或将该图片保存到磁盘。我们的客户一直在抱怨间歇性的 AccessViolationExceptions。经过一个下午的大部分时间后,我找到了一种可靠的方法来在我的开发环境中重现该问题,方法是在将 PictureBox.Image 保存到 MemoryStream 之前强制 GC.Collect

我们像这样填充 PictureBox 控件:

int w = videoInfoHeader.BmiHeader.Width;
int h = videoInfoHeader.BmiHeader.Height;
int stride = w * 3;

GCHandle handle = GCHandle.Alloc(savedArray, GCHandleType.Pinned);
long scan0 = (long)handle.AddrOfPinnedObject();
scan0 += (h - 1) * stride;  //image is upside down, so start at the bottom (I guess bottom-up bitmaps still scan left-to-right?)
Bitmap b = new Bitmap(w, h, -stride, PixelFormat.Format24bppRgb, (IntPtr)scan0);
handle.Free();
Image old = pictureBog.Image;
pictureBox.Image = b;
if (old != null)
    old.Dispose();
//Show picturebox, and let user use the form

当用户想要编辑照片时,我们将位图作为 MemoryStream 拉出,格式为 Jpeg(我不知道为什么):

//GC.Collect()
using (MemoryStream stream = new MemoryStream())

    pictureBox.Image.Save(stream, ImageFormat.Jpeg);  //AccessViolationException here if GC.Collect() is uncommented.
    byte[] pic = stream.ToArray();
    //Do stuff with pic and open editor form

我注意到,从Randomly occurring AccessViolationException in GDI+ 开始,存在 GC 将内容从非托管代码下移出的问题,因此您必须“固定”您的托管对象以防止这种情况发生。我看到我们在填充PictureBox 时正在这样做,但在我们将图像从PictureBox 中拉出时却没有。我知道需要“固定”源字节数组,因为字节数组只是一个哑数组。但是,我什至不知道在保存图像时我会尝试“固定”什么 - MemoryStream?我想 Bitmap.Save(Stream) 会足够聪明,可以在需要时自动处理。另外,缓冲区将需要重新分配,因为它无论如何都会填满,所以固定对我来说没有多大意义。任何人都知道是什么导致了错误的内存访问?

我想最坏的情况是我们在提取图像数据时避免使用PictureBox,保留原始字节数组,从中构建Bitmap并从新副本中保存。这似乎可行,但我不知道这是否只是巧合,真正的问题仍然存在。

【问题讨论】:

查看位图构造函数的 MSDN 文档。有一句话:“调用者负责分配和释放scan0参数指定的内存块,但是,在释放相关的Bitmap之前不应释放内存。”您正在释放代码中的固定内存,但不再使用位图。如果您立即执行垃圾回收,您也会立即得到相应的异常。否则,当 GC 决定运行时,异常可能会在将来的任何时间发生。 【参考方案1】:

从字节数组生成图像的更安全的方法是首先使用简单的new Bitmap(width, height, pixelformat) 构造函数生成图像本身,然后使用LockBits 打开其支持数据,然后将数据复制到Scan0它暴露的指针,逐个扫描线,使用Marshal.Copy。这样,您就不会弄乱可能导致问题的非托管指针。唯一涉及的指针是由LockBits 保留的专门锁定的内存,在调用unlockBits 之后,它再次安全地成为内部图像数据的一部分。

代码可以在这里找到:

A: Why must "stride" in the System.Drawing.Bitmap constructor be a multiple of 4?

(答案相同,但问题大不相同,所以我认为将其标记为重复没有太大用处)

请注意,该方法中包含对倒置步幅的处理。

【讨论】:

以上是关于垃圾回收后立即尝试保存 PictureBox.Image 时出现 AccessViolationException的主要内容,如果未能解决你的问题,请参考以下文章

java的while循环中被new的对象在一次循环结束后 会被垃圾回收吗?

JVM之垃圾回收相关概念

垃圾回收与对象的引用

面试题-Java基础-垃圾回收

Java练习题 05

C# 垃圾回收