我们有一个应用程序,其中包含一个与摄像头连接的表单。用户可以从表单控制相机,用它拍照,在WinForms PictureBox
中显示,打开另一个表单来编辑正在显示的图片或将该图片保存到磁盘。我们的客户一直在抱怨间歇性的AccessViolationExceptions。在一个下午的大部分时间之后,我找到了一种可靠的方法,通过在将GC.Collect
保存到PictureBox.Image
之前强制MemoryStream
来重现我的开发环境中的问题。
我们填充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
}
我注意到,从Randomly occurring AccessViolationException in GDI+开始,GC存在从非托管代码中移出内容的问题,因此您必须“固定”托管对象以防止这种情况发生。我看到我们在填充PictureBox
时正在执行此操作,但是当我们将图像从PictureBox
中拉出时,我们就会这样做。我理解需要“固定”源字节数组,因为字节数组只是一个哑数组。但是,我甚至不知道在保存图像时我会尝试“固定” - MemoryStream?我认为Bitmap.Save(Stream)足够聪明,可以在需要时自动处理。加上缓冲区需要重新分配,因为它总是填满,所以钉扎对我来说没有多大意义。任何人都知道导致内存访问不良的原因是什么?
我认为最坏的情况是我们在提取图像数据时避免使用PictureBox
,保留原始字节数组,从中构建Bitmap
并从新副本中保存。这似乎有效,但我不知道这是否只是巧合,真正的问题仍然存在。
答案 0 :(得分:0)
从字节数组中生成图像的一种更安全的方法是首先使用简单的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?
(答案是一样的,但问题却截然不同,所以我认为将此标记为副本有很多用处)
请注意,方法中包含倒置步幅的处理。