c#中的Bitmap类型是很重要的类型,常常感到困惑,自己本次做了一些查询与测试,总结如下:

一、主要情况说明

(一)Bitmap类

该类是CLR管理的类型,但其图像数据存储空间是否收到CLR管理不是很清楚。不过在使用的时候,是无需手工调用Dispose来释放数据内存空间的,GC会自动释放内存控件的。当然手工调用Dispose的话,GC确实会释放其数据内存空间的,只不过内存空间是托管的还是非托管的没搞清楚。不过对于使用来说,如果需要及时释放内存空间的话,可以调用Dispose,比如包含在using()中来使用。如果不占用很多内存空间,不调用Dispose也完全没有关系的,不会造成内存泄漏,但尽可能及时Dispose。

 

(二)Bitmap类的GetHBitmap()方法

IntPtr imagePtr=bitmap.GetHBitmap();

本来以为GetHBitmap()方法是返回一个指向图像数据的IntPtr而已,然而并不是这样的,实际上是创建新的对象,数据都是完全复制过来,不是指向原来bitmap的数据。

该方法会创建一个新的GDI位图,并将指针(句柄)包装成IntPtr返回。特别要注意的是,这是创建一个新的位图,创建完成后,imagePtr就与原来的bitmap完全没有关系了,新的位图完全占用自己的内存空间。需要注意的是,IntPtr类型变量当离开作用域空间且没有其它对象应用它,GC不会释放其内存空间的,必须由程序员手工释放,即调用DeleteObject()方法释放。此外,InPtr所指向的数据应该是32位的,即使原来Image是24位的,IntPtr图像也是32位的。

 

(三)利用imagePtr来创建ImageSource对象

ImageSource imgSource = Imaging.CreateBitmapSourceFromHBitmap(imagePtr, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());

利用imagePtr来创建ImageSource对象,也是一样会创建一个新的imgsource对象且图像数据完全拷贝到新对象中,相当于深拷贝,即整个数据都回拷贝过来,然后imgSource就与imagePtr没有关系了。bitmapSource没有实现IDisposable接口,因此不存在Dispose方法。不过,GC可以释放其内存空间。对于需要及时释放内存空间的,可以直接调用GC.Collect(),注意不要过于频繁调用GC.Collect(),避免影响性能。此外,ImageSource图像所指向的数据应该是32位的,即使原来创建IntPtr的Bitmap是24位的,ImageSource图像也是32位的。

 

(四)Bitmap.LockBits方法

BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

 

该方法本来以为是简单的锁定Bitmap的数据,返回BitmapData对象。(BitmapData对象中有一个Intptr类型的Scan0属性,指向具体数据)。但是,不一定的,不一定的,不一定的…是简单的锁定,也可能新创建数据块并转换数据格式后锁定。MSDN中并没有提及这一点。如果LockBits方法中的PixelFormat参数和原来bitmap的PixelFormat属性相同,则就是简单锁定。如果不同,则格式可以接受(32位和24位互相可以接受,其它没有一一测试),比如原来bitmap的PixelFormat属性是PixelFormat.Format24bppRgb,而LockBits中参数给了PixelFormat.Format32bppRgb,也就是图片明明时24位的,你锁定的时候非要按32位来锁定,那么系统就回创建一个新的IntPtr数据区块,并将原来的24位的数据转换位32位的数据存放中新的IntPtr指针对应的内存中。当后面调用bitmap.UnlockBits(data)的时候,系统会自动释放掉新创建的IntPtr所指向的内存数据块并回收内存。反过来也是一样的,即假设原来是32位的,LockBits中设置为24位,则会新创建一个数据块出来,若也设置位32位的,则不会新创建一个数据块出来。

 

二、代码测试

以下针对上述问题写了些代码,做了简单测试。测试过程中,利用三张图片,mm2000.bmp(2000*2000像素,24位色,理论上占用约12M空间),mmNew2000.bmp(2000*2000像素,24位色,理论上占用约12M空间),Separator100b24.bmp(100*100像素,24位色,理论上占用约30k空间)。

class Program
    {
        [DllImport("gdi32")]
        static extern int DeleteObject(IntPtr o);

        /// <summary>
        /// print Memory State and return current memory size
        /// </summary>
        /// <param name="previousMemory"> Memory Size at previous step</param>
        /// <returns>current Memory Size</returns>
        private static double PrintMemoryState(double previousMemory)
        {
            Process CurrentProcess = Process.GetCurrentProcess();
            double oldsizeD = previousMemory,
                   currentSizeD = ((double)CurrentProcess.WorkingSet64) / 1024 / 1024;
            string sizeString ="Current Memory Size="+currentSizeD.ToString("F2") + "M  ";//占用内存
            double sizeChangedD = currentSizeD - oldsizeD;
            string sizeChangedStr = ";sizeChanged=" + (sizeChangedD > 0 ? ("+" + sizeChangedD.ToString("F2")) : sizeChangedD.ToString("F2")) + "M";
            Console.WriteLine(sizeString+sizeChangedStr);
            Console.WriteLine();
            return currentSizeD;
        }

        static void Main(string[] args)
        {
            double previousMemory = 0;
            Console.WriteLine("Start Process");
            previousMemory = PrintMemoryState(previousMemory);
          
            Console.WriteLine("Just before pic mm2000.bmp is loading");
            previousMemory = PrintMemoryState(previousMemory);

            //Load pic from file and create a bitmap object,
            Bitmap bitmap = (Bitmap)Bitmap.FromFile(@"E:\mm2000.bmp");        
            Console.WriteLine("Pic mm2000.bmp(2000*2000 RGB24bits) has been loaded  to bitmap for 1st time");
            Console.WriteLine("PixelFormat=" + bitmap.PixelFormat.ToString());
            previousMemory = PrintMemoryState(previousMemory);

            bitmap.Dispose();
            bitmap = null;        
            Console.WriteLine(" Pic mm2000.bmp has been unloaded for 1st time");
            previousMemory = PrintMemoryState(previousMemory);


            bitmap = (Bitmap)Bitmap.FromFile(@"E:\mm2000.bmp");
            Console.WriteLine(" Pic file mm2000.bmp(2000*2000 RGB24bits) has been loaded to bitmap for 2nd time");
            Console.WriteLine(" PixelFormat=" + bitmap.PixelFormat.ToString());
            previousMemory = PrintMemoryState(previousMemory);


            Bitmap moreBitmap = (Bitmap)Bitmap.FromFile(@"E:\mm2000.bmp");
            Console.WriteLine(" Pic file mm2000.bmp(2000*2000 RGB24bits) has  been loaded to moreBitmap for 1st time");
            Console.WriteLine(" PixelFormat=" + moreBitmap.PixelFormat.ToString());
            previousMemory = PrintMemoryState(previousMemory);

            Bitmap moreBitmapNew = (Bitmap)Bitmap.FromFile(@"E:\mmNew2000.bmp");
            Console.WriteLine(" Pic file mmNew2000.bmp(2000*2000 RGB24bits) has  been loaded to moreBitmapNew for 1st time");
            Console.WriteLine(" PixelFormat=" + moreBitmap.PixelFormat.ToString());
            previousMemory = PrintMemoryState(previousMemory);

            Bitmap more100Bitmap = (Bitmap)Bitmap.FromFile(@"E:\Separator100b24.bmp");
            Console.WriteLine("Pic file Separator100b24.bmp(100*100 RGB24bits) has been loaded to more100Bitmap");
            Console.WriteLine("PixelFormat=" + moreBitmap.PixelFormat.ToString());
            previousMemory = PrintMemoryState(previousMemory);

            //Create a New Bitmap with specified PixelFormat
            int iwidth = bitmap.Width;
            int iHeight = bitmap.Height;
            Bitmap bmNew = new Bitmap(iwidth, iHeight, PixelFormat.Format24bppRgb);
            Graphics g = Graphics.FromImage(bmNew);
            g.DrawImage(bitmap, new System.Drawing.Point(0, 0));
            g.Dispose();           
            Console.WriteLine("New Bitmap bmNew Has been Created");
            Console.WriteLine("bmNew PixelFormat=" + bmNew.PixelFormat.ToString());
            previousMemory = PrintMemoryState(previousMemory);


            BitmapData data = bmNew.LockBits(new System.Drawing.Rectangle(0, 0, bmNew.Width, bmNew.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppRgb);

           
                Console.WriteLine("bmNew has been locked With parameter PixelFormat=PixelFormat.Format32bppRgb");
                Console.WriteLine("The Stride="+ data.Stride);
                previousMemory = PrintMemoryState(previousMemory);

            bmNew.UnlockBits(data);      
            Console.WriteLine("bitmapData has been unlocked");
            previousMemory = PrintMemoryState(previousMemory);

            IntPtr imagePtr = bitmap.GetHbitmap();        
            Console.WriteLine("imagePtr(IntPtr) has been created from bitmap");
            previousMemory = PrintMemoryState(previousMemory);

            BitmapSource bitmapSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(imagePtr, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
            Console.WriteLine("imgsource has been created  from imagePtr");
            previousMemory = PrintMemoryState(previousMemory);


            bitmap.Dispose();
            Console.WriteLine("bitmap has been disposed");
            previousMemory = PrintMemoryState(previousMemory);

            moreBitmap.Dispose();
            Console.WriteLine("moreBitmap has been disposed");
            previousMemory = PrintMemoryState(previousMemory);

            moreBitmapNew.Dispose();
            Console.WriteLine("moreBitmapNew has been disposed");
            previousMemory = PrintMemoryState(previousMemory);

            more100Bitmap.Dispose();
            Console.WriteLine("more100Bitmap has been disposed");
            previousMemory = PrintMemoryState(previousMemory);

            bmNew.Dispose();
            Console.WriteLine("bmNew has been disposed");
            previousMemory = PrintMemoryState(previousMemory);

            bitmapSource = null;          
            Console.WriteLine("imgsource Has set to null");
            previousMemory = PrintMemoryState(previousMemory);

            GC.Collect();
            GC.WaitForPendingFinalizers();    
            Console.WriteLine("Garbage Collected,bitmapSource should be realeased");
            previousMemory = PrintMemoryState(previousMemory);


            DeleteObject(imagePtr);
            Console.WriteLine("DeleteObject has been executed,imagePtr data should be realeased");
            previousMemory = PrintMemoryState(previousMemory);


            Console.WriteLine("Process Ended");
            Console.Read();
        }
    }

在上面代码中,对bmNew位图的锁定时,指定了PixelFormat.Format32bppRgb参数,这和bnNew位图自身的格式PixelFormat.Format24bppRgb是不相同的,底层数据存储也是不兼容的。执行后的结果如下所示:

Start Process
Current Memory Size=14.03M  ;sizeChanged=+14.03M

Just before pic mm2000.bmp is loading
Current Memory Size=14.52M  ;sizeChanged=+0.49M

Pic mm2000.bmp(2000*2000 RGB24bits) has been loaded  to bitmap for 1st time
PixelFormat=Format24bppRgb
Current Memory Size=40.59M  ;sizeChanged=+26.08M

 Pic mm2000.bmp has been unloaded for 1st time
Current Memory Size=17.74M  ;sizeChanged=-22.85M

 Pic file mm2000.bmp(2000*2000 RGB24bits) has been loaded to bitmap for 2nd time
 PixelFormat=Format24bppRgb
Current Memory Size=41.24M  ;sizeChanged=+23.50M

 Pic file mm2000.bmp(2000*2000 RGB24bits) has  been loaded to moreBitmap for 1st time
 PixelFormat=Format24bppRgb
Current Memory Size=64.85M  ;sizeChanged=+23.61M

 Pic file mmNew2000.bmp(2000*2000 RGB24bits) has  been loaded to moreBitmapNew for 1st time
 PixelFormat=Format24bppRgb
Current Memory Size=88.69M  ;sizeChanged=+23.84M

Pic file Separator100b24.bmp(100*100 RGB24bits) has been loaded to more100Bitmap
PixelFormat=Format24bppRgb
Current Memory Size=89.07M  ;sizeChanged=+0.38M

New Bitmap bmNew Has been Created
bmNew PixelFormat=Format24bppRgb
Current Memory Size=101.94M  ;sizeChanged=+12.87M

bmNew has been locked With parameter PixelFormat=PixelFormat.Format32bppRgb
The Stride=8000
Current Memory Size=118.25M  ;sizeChanged=+16.31M

bitmapData has been unlocked
Current Memory Size=103.41M  ;sizeChanged=-14.84M

imagePtr(IntPtr) has been created from bitmap
Current Memory Size=122.04M  ;sizeChanged=+18.63M

imgsource has been created  from imagePtr
Current Memory Size=140.05M  ;sizeChanged=+18.01M

bitmap has been disposed
Current Memory Size=117.59M  ;sizeChanged=-22.46M

moreBitmap has been disposed
Current Memory Size=94.79M  ;sizeChanged=-22.81M

moreBitmapNew has been disposed
Current Memory Size=72.61M  ;sizeChanged=-22.17M

more100Bitmap has been disposed
Current Memory Size=72.82M  ;sizeChanged=+0.21M

bmNew has been disposed
Current Memory Size=61.67M  ;sizeChanged=-11.15M

imgsource Has set to null
Current Memory Size=62.44M  ;sizeChanged=+0.77M

Garbage Collected,bitmapSource should be realeased
Current Memory Size=47.57M  ;sizeChanged=-14.87M

DeleteObject has been executed,imagePtr data should be realeased
Current Memory Size=32.31M  ;sizeChanged=-15.26M

Process Ended

从执行结果可以发现:

(1)从文件创建的Bitmap占用内存是位图本身大小的2倍左右,不知道是不是持有文件Stream流导致的。直接创建的Bitmap和位图本身数据大小差不多,从上面的newBM的创建与释放过程的内存变化可以看出来。

(2)由于LockBits中的参数给了32位,而实际位图是24位,因此数据存储是不兼容的,所以发现在LockBits以后,内存增加了16M多一些,UnLockBits后,内存又下降了14M多,推断是系统重新创建了内存区域,并把24位数据转换位32位数据存储到锁定区域,UnLockBits时又释放了。当然,如果在LockBits时指定的格式也是24位的,也就是格式和bitmap的格式一致,则测试过程中,内存基本没什么变化。因此,结论是:LockBits中给出的PixelFormat如果与bitmap本身兼容,则不会创建新的数据区块,否则会创建新的数据区块,并在UnLockBits时释放掉。

(3)IntPtr imagePtr=bitmap.GetHBitmap()方法创建了新的位图并且数据是从bitmap完全拷贝到imagePtr所指的新的内存区块中的,并非共享数据的。此外,需要DeleteObject来释放imagePtr,这一点非常重要,否则clr是无法回收内存的。

(4)ImageSource imgSource = Imaging.CreateBitmapSourceFromHBitmap(...)方法创建了imgSource 并且数据是从bitmap完全拷贝到imgSource 所指的新的内存区块中的。clr是可以回收imgSource对象的。

(5)在测试过程中,不同时间执行的结果变化还是又一定差异的,尤其是Bitmap.Dispose()执行后差异较大。这种方式查看内存变化也只能作为参考,不能作为某一个对象具体占用或释放了多少内存的依据。

 

最后补充:如果上面测试代码中的LockBit修改一下,将指定的32位改为24位,也就是与24位的位图自身的格式相同,如下:

BitmapData data = bmNew.LockBits(new System.Drawing.Rectangle(0, 0, bmNew.Width, bmNew.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);

 

那么针对锁定前后的执行结果如下,可以发现内存变化就很小了:

bmNew has been locked With parameter PixelFormat=PixelFormat.Format32bppRgb
The Stride=6000
Current Memory Size=104.89M  ;sizeChanged=+0.41M

bitmapData has been unlocked
Current Memory Size=105.15M  ;sizeChanged=+0.26M