善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。

2025-08-05

是谁动了我的 HDC?

最近工作上倒是查了不少问题,然而到了末尾都发现只是一些低级错误,完全不好意思拿出来说。但今天遇到一个,还算有一些意思,可以讲讲。


问题的表现是我们的软件上某些部位偶尔会「花」掉,显示的是之前覆盖在上面的内容。有经验的程序员一看就知道是 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 版本可能会有不一样的情况。毕竟是所谓的「未定义」行为。

没有评论:

发表评论