You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Bitmap.LockBits和Marshal.Copy填充索引位图时如何避免堆损坏

非8bpp索引位图创建的堆损坏问题解决方案

1. IndexedBitmapHelper.SetPixels方法的核心问题

你的实现大概率没处理GDI+位图的行字节对齐要求:所有GDI+位图(包括索引格式)的每行像素数据必须是4字节的整数倍,不足的部分会自动添加填充字节。如果代码只计算了像素本身的字节数(比如宽度×每像素位数/8),没把填充字节算进去,用Marshal.Copy写入时就会越界——要么抛出ArgumentOutOfRangeException,要么触发堆损坏。

另外,若你直接按像素数对应的字节长度分配源数组,没有匹配位图实际的内存大小(Stride×高度),写入时会覆盖不属于位图的内存区域,这也是堆损坏的直接原因。

2. 如何防止写入非位图内存?

核心是严格基于Bitmap.LockBits返回的BitmapData对象来操作,完全遵循GDI+的内存布局规则:

  • 用BitmapData.Stride获取每行的实际字节数(包含填充字节,必然是4的倍数),不要自行计算宽度×每像素位数/8。
  • 计算位图总内存大小:Stride × 高度,源数组的长度必须严格等于这个值,不能多也不能少。
  • 写入时要么一次性写入完整的匹配数组,要么逐行处理——每次只写入一行的Stride字节(像素数据+填充字节),避免越界。
  • 用完位图后必须调用Bitmap.UnlockBits释放锁定的内存,杜绝内存泄漏或堆结构破坏。

示例核心代码:

public static void SetPixels(Bitmap bitmap, byte[] pixelData)
{
    using (var bitmapData = bitmap.LockBits(
        new Rectangle(0, 0, bitmap.Width, bitmap.Height),
        ImageLockMode.WriteOnly,
        bitmap.PixelFormat))
    {
        int totalRequiredBytes = bitmapData.Stride * bitmapData.Height;
        if (pixelData.Length != totalRequiredBytes)
            throw new ArgumentException("源数组长度必须匹配位图实际内存大小");

        Marshal.Copy(pixelData, 0, bitmapData.Scan0, totalRequiredBytes);
    }
}

3. 额外字节的本质与计算方式

这些额外字节就是行对齐填充字节,GDI+要求每行字节数为4的倍数是为了优化CPU内存访问效率。

  • 先计算单行列数的原始像素字节数:rowPixelBytes = (bitmap.Width * BitsPerPixel + 7) / 8(比如4bpp格式下,每2个像素占1字节,宽度为奇数时,rowPixelBytes就是(宽度+1)/2)。
  • 填充字节数:paddingBytes = (4 - (rowPixelBytes % 4)) % 4(如果rowPixelBytes已经是4的倍数,填充0字节)。
  • 每行总字节数(即Stride):rowPixelBytes + paddingBytes。

填充字节的内容可以是任意值(0或其他),因为GDI+渲染图像时会忽略这些填充字节,不会影响显示效果。但必须确保源数组包含这些填充字节,否则写入时会越界,触发内存异常。

针对GIF编解码的优化:
因为GIF支持1/2/4/8bpp的索引格式,你可以在解码时直接生成带行对齐的像素数组,或者在写入位图前,将原始无填充的像素数据转换为符合Stride要求的数组——比如处理4bpp图像时,把原始每行像素数据复制到目标行的前rowPixelBytes位置,后面补填充字节即可,这样能最大化内存效率,无需转成8bpp格式。

内容的提问来源于stack exchange,提问作者sbridewell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 08:35:22