最近工作上倒是查了不少问题,然而到了末尾都发现只是一些低级错误,完全不好意思拿出来说。但今天遇到一个,还算有一些意思,可以讲讲。
问题的表现是我们的软件上某些部位偶尔会「花」掉,显示的是之前覆盖在上面的内容。有经验的程序员一看就知道是GDI的问题,但到底是什么问题呢?
![]() |
图片由Google Gemini生成 |
刚开始的时候,以为是GDI泄漏了。曾经有过一个例子,漏到了好几千,后来到了1万,触发了CEF的CollectGDIUsageAndDie被Dump了。还没到1万的时候,界面上的表现就是开始花,其实就是有部分GDI函数已经开始调用失败了。
然而这次并不是,GDI对象数很正常。开发人员远程调试跟了一下,发现是HDC拿不到。CreateCompatibleDC()得到了一个NULL,因此MemDC创建不出来。
什么情况下CreateCompatibleDC()会调用失败呢?GDI对象数不多,内存也很充足。软件中其它部位的MemDC是能正常创建的,因此全局性的因素都可以排除。什么光栅设备、显卡驱动之类也就不可能是原因了。
除此之外,就只剩下了一个可能性:CreateCompatibleDC()传进去的HDC参数有问题。
试了一下,乱传一个不存在HDC,的确会导致CreateCompatibleDC()返回NULL。如果传的是个NULL进去,倒是还好一点。看来是窗口上的HDC有问题,当然也有可能是窗口本身就有问题。然而其它地方也在用同一个HWND创建MemDC,一切正常。所以还是某些HDC有问题。
HDC的值看起来并不奇怪,也没有什么好办法确认它到底有什么问题。HWND还能用Spy++来看,HDC我是一筹莫展。还好可以OutputDebugString。日志打出来,有意思的地方来了。
我看出问题了:当某个HDC失效的时候,它一定是被连续拿到过两次,中间没有ReleaseDC()过。这两次GetDC(),都得到了同一个HDC,肯定不正常。
我把this指针和HWND的值也加到了日志里面,这下看得更清楚了。两次GetDC()分别是不同的HWND。而HDC失效之时,就是当它被最终ReleaseDC()的时候。只不过这次ReleaseDC()的HWND是一个旧的窗口,那个窗口此时应该已经被销毁了。
窗口OnDestroy()的时候,没能把它的HDC一并归还,这当然是我们软件中的代码错误,也是我们遇到的问题的直接原因。但故障现象的根本原因是什么呢?我们确实没有按照规范「一借一还」,至少窗口还「在世」的时候没有。不过ReleaseDC()难道不管三七二十一,只要有人拿着某个HDC来释放,它就答应吗?都不用看看HWND对不对得上吗?
微软关于ReleaseDC()的API函数说明在这一点上就有点语焉不详了。大概它没想到有人会这样去实践?的确也没人问这种问题,完全找不到资料,只好自己动手做了个实验:
HDC hDC = ::GetDC(hWnd); int nRet = ::ReleaseDC(hWnd + 0x1000, hDC);
随后再在这个DC上用GDI函数画东西,确实画不出来。nRet也的确是1,按照微软的说法,返回值1表示DC被释放,这倒是没有骗人。我把hDC也加了个数字,然后再跑一遍,这次nRet变成0了。
所以说,微软是在ReleaseDC()的时候搞了个「容错」逻辑?只要HDC对得上,就给释放,不管HWND对不对得上号?我一开始跟Google Gemini探讨这个问题的时候,它还不相信,直到我告诉它测试结果。
回过头来看,为什么两次GetDC()能得到同一个HDC呢?
我们的主窗口,经历了销毁后重建的过程。在OnDestroy()的时候,没有及时执行ReleaseDC()。但Windows可能认为,窗口不在了,DC也就没了。于是另一个新建起来的窗口又通过GetDC()拿到了同一个HDC。等到主窗口的C++对象开始析构,调用ReleaseDC()的时候,新窗口拿到的HDC就被背刺了。系统的DC应该是在放一个池子里面,所以是有可能被重用的。这当然需要「运气」,也正因为如此,故障现象不是很稳定。
会遇到这种问题的人,应该不多。现在还在用GDI做开发的项目本来也就不多了。我在网上没能找到什么可以参考的信息,还好勉强算能够重现,就抓住机会解决了问题。经验值又+1了。
最后说明一下:我这个实验是用VS2013在Win10下面做的。不同的OS以及VS版本可能会有不一样的情况。毕竟是所谓的「未定义」行为。
没有评论:
发表评论