Bitmap.Clone()和new Bitmap(Bitmap)有什么区别?

81

据我所知,复制位图有两种方法。

Bitmap.Clone()

Bitmap A = new Bitmap("somefile.png");
Bitmap B = (Bitmap)A.Clone();

new Bitmap()

Bitmap A = new Bitmap("somefile.png");
Bitmap B = new Bitmap(A);

这些方法有什么不同?我特别关注于内存和线程方面的区别。


3
我曾遇到这样一种情况,我要读取的文件是一个每像素1比特的TIFF文件。使用new Bitmap(A)返回32比特每像素的位图,而(Bitmap)A.Clone()仍然是每像素1比特。由于我要将图像嵌入PDF以便以后发送电子邮件,因此保持图像为1比特很重要。@Aelios @HansPassant - gmlobdell
2个回答

118

阅读之前的答案,我担心像素数据会在位图的克隆实例之间共享。因此,我进行了一些测试,以查明Bitmap.Clone()new Bitmap()之间的区别。

Bitmap.Clone() 会保持原始文件被锁定:

  Bitmap original = new Bitmap("Test.jpg");
  Bitmap clone = (Bitmap) original.Clone();
  original.Dispose();
  File.Delete("Test.jpg"); // Will throw System.IO.IOException

使用new Bitmap(original)代替后,将在original.Dispose()之后解锁文件,并且不会抛出异常。使用.Clone()创建的克隆体来使用Graphics类进行修改时,不会修改原始对象:

  Bitmap original = new Bitmap("Test.jpg");
  Bitmap clone = (Bitmap) original.Clone();
  Graphics gfx = Graphics.FromImage(clone);
  gfx.Clear(Brushes.Magenta);
  Color c = original.GetPixel(0, 0); // Will not equal Magenta unless present in the original

同样地,使用LockBits方法会为原始的和克隆的对象产生不同的内存块:
  Bitmap original = new Bitmap("Test.jpg");
  Bitmap clone = (Bitmap) original.Clone();
  BitmapData odata = original.LockBits(new Rectangle(0, 0, original.Width, original.Height), ImageLockMode.ReadWrite, original.PixelFormat);
  BitmapData cdata = clone.LockBits(new Rectangle(0, 0, clone.Width, clone.Height), ImageLockMode.ReadWrite, clone.PixelFormat);
  Assert.AreNotEqual(odata.Scan0, cdata.Scan0);

使用object ICloneable.Clone()Bitmap Bitmap.Clone(Rectangle, PixelFormat)所得到的结果是相同的。
接下来,我使用以下代码进行了一些简单的基准测试。
将50份副本存储在列表中花费了6.2秒,并导致1.7 GB的内存使用(原始图像为24 bpp,3456 x 2400像素=25 MB)。
  Bitmap original = new Bitmap("Test.jpg");
  long mem1 = Process.GetCurrentProcess().PrivateMemorySize64;
  Stopwatch timer = Stopwatch.StartNew();

  List<Bitmap> list = new List<Bitmap>();
  Random rnd = new Random();
  for(int i = 0; i < 50; i++)
  {
    list.Add(new Bitmap(original));
  }

  long mem2 = Process.GetCurrentProcess().PrivateMemorySize64;
  Debug.WriteLine("ElapsedMilliseconds: " + timer.ElapsedMilliseconds);
  Debug.WriteLine("PrivateMemorySize64: " + (mem2 - mem1));

使用Clone(),我可以在0.7秒内使用0.9GB存储列表中的1,000,000个副本。如预期的那样,与new Bitmap()相比,Clone()非常轻量级:

  for(int i = 0; i < 1000000; i++)
  {
    list.Add((Bitmap) original.Clone());
  }

Clone() 方法创建的克隆对象采用写时复制技术。在这里,我会在克隆对象中将一个随机像素点改为随机颜色。这个操作似乎会触发从原始对象复制所有像素数据,因为我们现在回到了7.8秒和1.6 GB:

  Random rnd = new Random();
  for(int i = 0; i < 50; i++)
  {
    Bitmap clone = (Bitmap) original.Clone();
    clone.SetPixel(rnd.Next(clone.Width), rnd.Next(clone.Height), Color.FromArgb(rnd.Next(0x1000000)));
    list.Add(clone);
  }

仅从图像中创建Graphics对象不会触发复制:

  for(int i = 0; i < 50; i++)
  {
    Bitmap clone = (Bitmap) original.Clone();
    Graphics.FromImage(clone).Dispose();
    list.Add(clone);
  }

为了触发复制操作,您需要使用 Graphics 对象绘制一些内容。与之相反,使用 LockBits 将复制数据,即使指定了 ImageLockMode.ReadOnly 也是如此:

  for(int i = 0; i < 50; i++)
  {
    Bitmap clone = (Bitmap) original.Clone();
    BitmapData data = clone.LockBits(new Rectangle(0, 0, clone.Width, clone.Height), ImageLockMode.ReadOnly, clone.PixelFormat);
    clone.UnlockBits(data);
    list.Add(clone);
  }

2
那么,哪种方法是获取完整独立图像和所有数据的最佳方法? - Moon
1
如果您需要一个单独的副本,我会使用 new Bitmap()。这不会在原始文件上保持文件锁定,并且所需的 CPU 时间和内存将在复制的位置使用,而不是在开始修改副本的位置使用。但是,如果您不确定副本是否会被修改,.Clone() 可能是更好的选择。 - Anlo
2
克隆-锁定位-解锁位的最后一部分使我能够通过克隆裁剪图像并覆盖其原始文件名。使用MemoryStream获取原始图像,使用Marshal.Copy,使用Graphics.FromImage以及通过MemoryStream保存都是由不同的人推荐的,但都失败了(在运行IIS7.5的Windows Server上出现问题,但在VS中没有问题)。 - monty

85

"深拷贝"和"浅拷贝"之间的常见区别,也是几乎过时的IClonable接口的问题。Clone()方法创建一个新的Bitmap对象,但像素数据与原始位图对象共享。Bitmap(Image)构造函数也创建一个新的Bitmap对象,但它具有自己的像素数据副本。

在SO上有许多关于Clone()的问题,程序员希望它避免位图的典型问题,即从中加载的文件被锁定。 但它并不会避免这个问题。可能实用的用法是避免库方法不适当地调用Dispose()来释放传递的位图。

重载可能很有用,可以利用像素格式转换或裁剪选项。


4
同意。在需要在多个地方使用相同(未修改)位图的情况下,我们使用Clone()函数,但是我们想要减少副本使用的内存量。我不知道的一件事是,如果您修改其中一个克隆(例如SetPixel),是否会导致所有共享像素数据被修改,还是它会导致修改的像素数据分配自己的像素数据(因此只修改自己的)。 - Matt Smith
@MattSmith,即使使用ReadOnly标志,数据也会在锁定命令之后被复制。 - Pedro77
非常感谢。这个“具有其自己的像素数据副本”是导致我的程序崩溃的原因。通过使用“new Bitmap(oldBitmap)”而不是“oldBitmap.Clone()”来解决了这个问题。 - Seth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接