尝试在垃圾收集后立即保存PictureBox.Image时发生AccessViolationException



我们有一个应用程序,它包含一个与相机接口的窗体。用户可以从表单控制相机,用它拍照,在WinFormsPictureBox中显示,打开另一个表单编辑正在显示的图片或将该图片保存到磁盘。我们的客户一直在抱怨间歇性的AccessViolationException。在一个下午的大部分时间后,我找到了一种可靠的方法,通过在将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

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

//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
}

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

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

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

代码可以在这里找到:

A: 为什么System.Drawing.Bitmap构造函数中的"步幅"必须是4的倍数?

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

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

最新更新