上次,我提到了可能导致正常的消息循环被破坏的怪异之处。
有一位读者 Adrian 指出,WM_GETMINMAXINFO 消息在顶级窗口 WM_NCCREATE 之前到达。这确实很不幸,但(无论是否错误)十多年来一直如此,现在修改它会引入严重的兼容性风险。
但这不是我想到的怪异之处。
前段时间,我正在帮助调试一个使用 ListView 控件的程序的问题,该问题可追溯到 ListView 控件子类化的程序,并通过复杂的C++对象链,最终尝试销毁 ListView 控件,而它已经在销毁过程中。
让我们以新的例子程序为例,以一种更加明显的方式来说明我所提到的问题。
我在代码中添加了一些调试跟踪,以便更轻松地查看正在发生的事情。运行程序,然后关闭它,并观察会发生什么。
哎呀!发生了什么事?
当您单击窗口右上角的关闭按钮时,这将启动窗口销毁过程。正如预期的那样,窗口收到了一条 WM_DESTROY 消息,但程序通过尝试再次销毁窗口来响应此消息。请注意,IsWindow 报告此时该窗口仍然存在。这是真的:窗口仍然存在,尽管它恰好正在被破坏的过程中。在原始场景中,破坏窗口的代码类似于下面的代码:
if (IsWindow(hwndToDestroy))
{
DestroyWindow(hwndToDestroy);
}
无论如何,对 DestroyWindow 的递归调用导致一个新的窗口销毁周期开始,嵌套在第一个窗口内。这将生成一条新的 WM_DESTROY 消息,后跟一条 WM_NCDESTROY 消息。(请注意,此窗口现在已收到两条 WM_DESTROY 消息!然后,我们的代码对DestroyWindow 进行了另一个递归调用,从而开始了第三个窗口销毁周期。窗口获取其第三条 WM_DESTROY 消息,然后是第二条 WM_NCDESTROY 消息,此时返回对 DestroyWindow 的第二次递归调用。此时,窗口不再存在:DestroyWindow 已销毁窗口。
这就是程序崩溃的原因。基类通过销毁与窗口关联的实例变量来处理 WM_NCDESTROY 消息。因此,当最里面的 DestroyWindow 返回时,实例变量已被丢弃。然后,使用基类的 WM_NCDESTROY 处理程序恢复执行,该处理程序尝试访问实例变量并获取堆垃圾,然后使释放已释放的内存变得更糟,从而损坏堆。正是在这里,我们崩溃了,试图在已经破坏的对象上调用虚拟析构函数。
我有意选择使用新的例子程序(使用C++对象)而不是经典的例子程序(使用全局变量)来强调这样一个事实,即在递归 DestroyWindow 调用之后,所有实例变量都消失了,你正在已经释放的内存上运行代码,这很危险。
故事的寓意:了解你的窗口的生命周期,不要破坏一个你知道已经处于破坏过程中的窗口。
总结
在非托管平台上编写代码如履薄冰,你需要时时谨慎,因为,不再有人能暗中保护你了。
最后
Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《When the normal window destruction messages are thrown for a loop》