垃圾回收后立即尝试保存 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的主要内容,如果未能解决你的问题,请参考以下文章