【问题标题】:Marshal.Copy method throws AccessViolationException in C#.NETMarshal.Copy 方法在 C#.NET 中引发 AccessViolationException
【发布时间】:2009-03-24 10:29:27
【问题描述】:

我正在开发一个可以显示来自相机的实时图像的 C# 应用程序。 我在使用以下代码 sn-p 时面临的问题是,当在线程中连续执行此函数时,我在 Marshal.Copy 中得到 AccessViolationException。但是,这在运行一次时会成功运行(我得到一个静态图像)。我想这与一些内存损坏问题有关。有关如何处理此问题的任何想法/建议?

    private Image ByteArrayToImage(byte[] myByteArray) 
    {
        if (myByteArray != null)
        {
            MemoryStream ms = new MemoryStream(myByteArray);
            int Height = 504;
            int Width = 664;
            Bitmap bmp = new Bitmap(Width, Height, PixelFormat.Format24bppRgb);
            BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
            Marshal.Copy(myByteArray, 0, bmpData.Scan0, myByteArray.Length);
            bmp.UnlockBits(bmpData);

            return bmp;
        }
        return null;
    }

【问题讨论】:

  • 不确定,但您能否尝试在“LockBits”和“UnlockBits”周围加锁。由于此语句将位图锁定在系统内存上。你的异常告诉了对受保护内存的无效访问。

标签: c# bitmap access-violation


【解决方案1】:

在我看来,您总是试图将字节数 myByteArray.Length 复制到位图缓冲区。

您并没有检查位图缓冲区实际上是否有那么大 - 所以可能正在注销位图缓冲区的末尾。

尝试检查 myByteArray.Length 是否大于 bmpData.Stride x bmp.Height

如果是这种情况,您需要重新审视您对宽度、高度和像素格式的硬编码值所做的假设。

【讨论】:

    【解决方案2】:

    您不应该一次复制整个图像。位图对象的内存分配可能不是您所期望的。例如,第一条扫描线可能最后存储在内存中,这意味着第二条扫描线的数据最终会在为位图对象分配的内存区域之外。扫描线之间也可能有填充以将它们放置在偶数地址上。

    一次复制一行,使用 bmpData.Stride 查找下一条扫描线:

    int offset = 0;
    long ptr = bmpData.Scan0.ToInt64();
    for (int i = 0; i < Height; i++) {
       Marshal.Copy(myByteArray, offset, new IntPtr(ptr), Width * 3);
       offset += Width * 3;
       ptr += bmpData.Stride;
    }
    

    【讨论】:

    • 也许我忽略了某些东西,但我看不到示例中每一行的指针有任何跳跃或差异。据我所知,这段代码与Marshal.Copy(myByteArray, 0, bmpData.Scan0, myByteArray.Length); 完全相同(长度为bmpData.Stride * bmp.Height)。
    • @LouisSomers 只有在图像自上而下存储在内存中的特殊情况下才会做同样的事情。 Stride 可能是负数。此外,扫描线之间有填充,但在最后一个扫描线之后可能没有填充。
    • bmpData.Stride 不会随着每次迭代而改变,它在循环中的每次迭代期间都是固定的长度。它不知道你是在向前还是向后循环,并且它不计算它被调用的次数。为了支持分段存储,它必须是一种采用行索引的方法。该文档还声明它“获取或设置位图对象的步幅宽度(也称为扫描宽度)。”。它不会在每次调用时都指向不同的位置。没什么大不了的,您的代码可以工作,但是对 Marshal.Copy 的一次调用应该会执行得更好。
    【解决方案3】:

    我个人见过一些堆损坏(调试故障转储),因为使用了 .Length。

    如:

    IntPtr ptr = bitmapdata.Scan0;
    Marshal.Copy(pixeldata, 0, ptr, pixeldata.Length);
    

    解决堆损坏的方法是用不同的方式计算 .Length:

    IntPtr ptr = bitmapdata.Scan0;
    int bytes = Math.Abs(bitmapdata.Stride) * bmp.Height;
    Marshal.Copy(pixeldata, 0, ptr, bytes);
    

    bytes 和 .Length 相差 1 个字节,导致堆损坏。

    Math.Abs​​ 直接取自 Microsoft 的示例。 因为对于自下而上的位图,Stride 可能是负数。

    例如微软:https://msdn.microsoft.com/en-us/library/system.drawing.imaging.bitmapdata.scan0%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396#Examples

    (+ 不要忘记 .Unlock 并将其添加到 try-finally 语句中。)

    【讨论】:

    • 好点。虽然不知何故知道什么是步幅,但我没有在数据块的长度中使用它。解决了我的问题——奇怪的是——只出现在一小部分案例中。
    【解决方案4】:

    替我回答:忘了->

            // Unlock the bits right after Marshal.Copy
            bmp.UnlockBits(bmpData);
    

    有人知道吗?这是没有答案的第四页。使用来自 msdn 的确切代码:http://msdn.microsoft.com/en-us/library/system.drawing.imaging.bitmapdata.aspx,即:

                            Bitmap bmp = new Bitmap("c:\\picture.jpg");
    
                            // Lock the bitmap's bits.  
                            Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
                            System.Drawing.Imaging.BitmapData bmpData =
                                bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
                                bmp.PixelFormat);
    
                            // Get the address of the first line.
                            IntPtr ptr = bmpData.Scan0;
    
                            // Declare an array to hold the bytes of the bitmap.
                            int bytes = bmpData.Stride * bmp.Height;
                            byte[] rgbValues = new byte[bytes];
    
                            // Copy the RGB values into the array.
                            //This causes read or write protected memory
                            System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
    

    这在优化模式下不起作用,并且在 IDE 中不作为 exe 运行。有任何想法吗 我试图把这是一个新项目,如果我在按下按钮时附加到进程,并多次按下此按钮会发生错误,但在我的代码中我只调用一次,无论哪种方式都不确定错误的原因。

    【讨论】:

    • 您忽略了一个像素大于一个字节的事实,具体取决于int bytes = bmpData.Stride * bmp.Height; 中的像素格式
    【解决方案5】:

    也许

    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);

    对于只写的参数无效 - 在 ImageLockMode 中尝试 ReadWrite 或 ReadOnly? 也许这有帮助。

    【讨论】:

      【解决方案6】:

      我搜索了一下,如果我们跳过数组大小不合适的可能性,我们会以Remarks for BitmapData.Stride Property结束:

      步幅是单行像素(一条扫描线)的宽度, 向上舍入到四字节边界。如果步幅为正,则 位图是自上而下的。 如果步幅为负,则位图为 自下而上。

      所以,也许我们应该这样做:

      BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
      Marshal.Copy(myByteArray, 0, bmpData.Scan0 + 
      ( bmpData.Stride >= 0 ? 0 : bmpData.Stride*(bmp.Height-1) ),
      myByteArray.Length);
      

      但我想知道:我们已经创建了位图:new Bitmap(Width, Height, PixelFormat.Format24bppRgb); ...所以,它怎么可能是负数?
      ILSpy的时间:

      public Bitmap(int width, int height, PixelFormat format)
      {
          IntPtr zero = IntPtr.Zero;
          int num = SafeNativeMethods.Gdip.GdipCreateBitmapFromScan0(width, height, 0, (int)format, NativeMethods.NullHandleRef, out zero);
          if (num != 0)
          {
                  throw SafeNativeMethods.Gdip.StatusException(num);
          }
          base.SetNativeImage(zero);
      }
      
      // System.Drawing.SafeNativeMethods.Gdip
      [DllImport("gdiplus.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
      internal static extern int GdipCreateBitmapFromScan0(int width, int height, int stride, int format, HandleRef scan0, out IntPtr bitmap);
      

      它指出了我herehere

      Bitmap(
        [in]  INT width,
        [in]  INT height,
        [in]  INT stride,
        [in]  PixelFormat format,
        [in]  BYTE *scan0
      );
      

      大步 [in]
      类型:INT
      一个整数,它指定一个扫描线的开头和下一个扫描线之间的字节偏移量。这通常(但不一定)是 像素格式的字节数(例如,2 表示每 16 位 像素)乘以位图的宽度。传递给 this 的值 参数必须是四的倍数。

      传0是什么意思?不知道,没找到。有人可以吗?可以用负步幅创建位图吗? (通过 .NET new Bitmap(Width, Height, PixelFormat.Format24bppRgb))。无论如何,我们至少需要检查BitmapData.Stride

      【讨论】:

        【解决方案7】:

        我在BitmapSource Class 的代码示例中找到了 rawStride 公式。似乎值得尝试使用下面的代码创建一个数组并尝试多次执行您的复制方法而不会被轰炸。如果可以的话,这很有可能是一个数组大小问题。如果您相机中的数据与内存中位图的大小不匹配,您可能必须逐行复制数据。

        private byte[] CreateImageByteArray(int width, int height, PixelFormat pixelFormat)
        {
            int rawStride = (width * pixelFormat.BitsPerPixel + 7) / 8;
            byte[] rawImage = new byte[rawStride * height];
        
            return rawImage;
        }
        

        你应该做的另一件事是确保当你完成 Bitmap 对象时它被正确处理。我偶尔会看到使用非托管代码对对象进行操作后未清理的奇怪结果。

        此外,跨线程传输对象有时会很棘手。请参阅 How to: Make Thread-Safe Calls to Windows Forms ControlsThread-Safe Calls Using Windows Form Controls in C# 文章。

        【讨论】:

          【解决方案8】:

          让我们尝试将 ThreadApartmentState 更改为 Single Threaded。

          另外,检查导致此错误的跨线程操作。

          【讨论】:

          • 你好。这确实应该是评论而不是答案(我知道您还没有足够的代表来发布 cmets)。
          猜你喜欢
          • 2019-11-30
          • 2019-05-11
          • 1970-01-01
          • 2021-11-28
          • 2021-12-18
          • 2022-07-07
          • 2014-12-03
          • 1970-01-01
          • 2013-03-29
          相关资源
          最近更新 更多