VC6中一个顽固很久CImageList的GDI泄露

 

程序开发告一段落,经过强力测试的时候,发现某处存在一个很不明显的GDI泄露问题。费了好久,追根溯源,居然发现是很早以前底层对图像列表CImageList封装导致。底层库出现问题,总会让我长吃一惊,吓出一身汗。解决问题是首要目的;另外分析下问题产生的原因,居然也发现一些很有意思的事情。

一.Bug重现

用最简单的代码来还原下bug产生的情况。如下代码演示了为图像列表添加图标的集中方法:从资源中添加,从文件中添加

void TestImagelist() 
{ 
 long  nCmdType = 0x0000; 

 CImageList oImageList;

 //1.0 创建空图像列表
 oImageList.Create(32,32,16,0,4);

 //1.1 创建列表并初始值
 //oImageList.Create(IDB_BMP_ICONSET, 32, 3, RGB(128,128,128));

 int nCount = 0;
 HICON hIcon = NULL;
 //2. 从资源中添加图标,不用释放
 if(nCmdType & 0x0001)
 {
  hIcon = LoadIcon(AfxGetResourceHandle(),MAKEINTRESOURCE(IDI_ICON1));
  if(hIcon)   
  {
   oImageList.Add(hIcon);
//DestroyIcon(hIcon); //不能调用;资源始终存在
  }  
  nCount = oImageList.GetImageCount();
 }

 //3. 从文件中添加图标,必须释放
 if(nCmdType & 0x0010)
 {
  hIcon = (HICON)LoadImage(NULL, _T("e:\\res\\icon1.ico"), IMAGE_ICON, 0, 0, LR_LOADFROMFILE); 
  if(hIcon)   
  {
   oImageList.Add(hIcon);
DestroyIcon(hIcon);  //必须调用;资源始终存在
  }
  nCount = oImageList.GetImageCount(); 
 }    
}

图像列表CImageList操作图标有好几种用法:

最开始使用时,都是把图标放在程序内部作为资源文件。或者从资源位图文件中一次装载所需的全部图标,调用如下函数:BOOL Create( UINT nBitmapID, int cx, int nGrow, COLORREF crMask );或者通过CImageList::Add添加零散图标。这两种方法都没有出现GDI泄露。

后来所有的界面,都是通过资源配置文件而来。大多数情况下是直接装载位图文件作为资源,如下:

int inAddBmp(CImageList& oImageList, LPCTSTR strBmpPath, COLORREF clrMask=RGB(0,0,0))
{
 HBITMAP hBmp = (HBITMAP)LoadImage( NULL ,
      strBmpPath,
IMAGE_BITMAP,0 , 0 ,LR_LOADFROMFILE ); 
 if(!hBmp)
  return 0;

 CBitmap oBmp; //析构时自动释放了bmp句柄
  oBmp.Attach(hBmp); 
 return oImageList.Add(&oBmp, clrMask);
}

后来也通过直接装载图标文件进行。前面的各种方式,都没有调用有DestroyIcon;而LoadImage(IMAGE_ICON)时也没有调用,而GDI泄露也就产生了。

二.资源句柄的工作原理

为什么直接通过资源文件装载,不需要进行句柄的释放呢?这需要理解资源文件和句柄的作用。

资源句柄是个什么东西?它和HWND和CWnd*一样。我们可以将资源句柄简单理解为一个资源对象的指针。句柄存在,该对象则一定存在。可能不是直接的对象指针这种对应关系;但是一定是一对一可以相互对应的关系。

同时,windows系统,对于每个进程的GDI的最大值有个限制,缺省时最大值是10000;超过10000程序就会挂掉。我们可以假想windows系统是如何管理这10000个句柄的。可能windows系统为每个进程单独分配了一个10000的句柄信息数组。进程中每次出现产生一个资源,就往该数组中添加一个值;每释放一个句柄,就在该数组中减少一个值。这样,可以每时每刻知道进程当前的资源句柄总数。超过10000,数组越界,程序就死了!数组中的每一项都是一个句柄信息。该信息可能包括句柄的类型,对应的对象指针(可能当时还没有对象说法呢,应该是内存位置、起始以及大小等)。

如果仅仅这样,微软设计出这套方式可能多此一举,相反还限定束缚了自己。它的另一个好处可能是对资源文件的管理。从文件中加载的资源文件,会在堆中产生一片内存;释放句柄时,既会在这个句柄信息数组中去掉一项,同时也会将这块堆内存给释放掉。而进程的资源,其早在程序启动时已经被加载到进程的数据段了,也就是说一直就在内存中;这块内存是不用释放的。这样,关闭句柄的时候,windows会自动判断该句柄的内存来源(数据段还是堆)。如果是堆,释放,同时在数组中去掉一个记录。如果是数据段,不用释放内存,只需要从数组中去掉即可。

但分析时发现,其实只要引用了一个资源文件,该资源文件句柄就一直常驻在数组中。第一次调用时,GDI会增长,即便DestroyIcon和ClodeHandle也不会减少。但是第二次调用时,GDI不会增长。只有在程序退出时,GDI会释放。并不会产生泄漏!可能一个句柄信息中,还包括资源ID。这样,对于第二次装载该资源时,会首先在数组中检查该ID是否存在,存在之则直接使用,不存在再装载!

三.分工合作

现在还能想象以前热衷搜集图标库的情景。当时在以前公司进行程序开发时,程序里面的一切都是由程序员做主。那时首先不怎么重视程序的易用和美观;另外一切资源都集成在代码中,也不方便程序和设计的分工合作。但是根还是在第一点,不重视产品的易用性;功能盖过一切。

现在,或者这家公司,在产品的易用性美观性方面精益求精,有专门的设计人员进行界面的设计。这是重要的改变。而将资源再放在代码中,很不利于设计开展工作。而通过资源配置,真正可以方便设计的自我配置和调试。设计人员给过来的或好或坏的东西,在程序员眼中,都是资源而已!或好或坏,那是设计人员的责任了。而设计人员能够方便的调试和配置,不需要和程序员打交道,那是之前程序员要达到的目标了。

这也是现在WPF和一切界面设计想追求的目标:美工的归设计,功能的归程序。真正达到,才是专业!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值