不,真的,你需要将所有未处理的消息传递给DefWindowProc,第二部分
摘要
这篇博客文章解释了为什么在Windows编程中,开发者必须将所有未处理的消息传递给DefWindowProc,并通过一个因不当清理处理导致的涉及RegisterDragDrop和RevokeDragDrop的内存泄漏案例进行说明。
<p>一位客户报告了Windows中的一个内存泄漏问题,该问题发生在他们调用<code>RegisterDragDrop</code>后接着调用<code>RevokeDragDrop</code>时。他们提供了一个示例程序的时间旅行跟踪,展示了这个问题。(尽管出于某种原因,他们没有提供程序本身;只提供了时间旅行跟踪。)</p>
<p>现在,如果调用<code>RegisterDragDrop</code>后接着调用<code>RevokeDragDrop</code>会导致内存泄漏,那就很奇怪了,因为这种模式被数千个应用程序大量使用,包括Windows本身的许多部分,所以如果这种模式本身有内存泄漏,你可能会认为它早就被报告了。</p>
<p>我怀疑他们的示例程序有些特殊之处。</p>
<p>不久前,我注意到<a title="No, really, you need to pass all unhandled messages to DefWindowProc" href="https://devblogs.microsoft.com/oldnewthing/20060425-16/?p=31413">不,真的,你需要将所有未处理的消息传递给DefWindowProc</a>。那就是问题的根源。</p>
<p>通过调试时间旅行跟踪,发现确实,他们调用了<code>RegisterDragDrop</code>,然后调用了<code>RevokeDragDrop</code>。但还有更多情况发生。当窗口收到<code>WM_DESTROY</code>消息时,它会清理所有状态。对于<code>WM_DESTROY</code>之后到达的任何消息,窗口过程会寻找其特殊状态但看不到,因此它放弃并返回0,而不将消息传递给<code>DefWindowProc</code>。</p>
<p>哎呀。</p>
<p>如果窗口过程不知道该做什么,它应该将所有消息传递给<code>DefWindowProc</code>。在这种情况下,这很重要,因为其中一些消息是清理消息,这些清理消息的作用之一是释放仍在周围的最后几个内存片段。</p>
<p><b>额外讨论</b>:但如果我注册一个拖放目标,然后撤销它,撤销操作不应该释放注册调用所分配的所有内存吗?</p>
<p>没有要求说注册某物然后注销它会立即释放与注册相关的所有内存。系统可以缓存它认为会再次需要的内容。</p>
<p>在这种情况下,发生的是<code>RegisterDragDrop</code>函数使用了一个由许多组件共享的基础设施。该基础设施在第一次有人需要时被创建并附加到窗口,并在窗口被销毁时清理。内存没有被泄漏。它只是缓存在窗口上,等待被另一个操作使用。缓存在窗口被销毁时被销毁。</p>
<p>但它假设你给了<code>DefWindowProc</code>一个机会来执行该清理。</p>
<p>文章<a href="https://devblogs.microsoft.com/oldnewthing/20260924-00/?p=112728/">不,真的,你需要将所有未处理的消息传递给DefWindowProc,第二部分</a>首次发表于<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>。</p>
查看缓存全文
缓存时间: 2026/09/25 02:41
# 不,真的,你需要将所有未处理的消息传递给DefWindowProc(第二部分)
来源:https://devblogs.microsoft.com/oldnewthing/20260924-00/?p=112728/
某客户报告了一个Windows内存泄漏问题,该问题在他们先后调用`RegisterDragDrop`和`RevokeDragDrop`时发生。他们提供了一个演示该问题的示例程序的时间旅行跟踪记录(Time Travel Trace)。(但不知为何,他们并未提供程序本身;仅提供了时间旅行跟踪记录。)
值得注意的是,如果调用`RegisterDragDrop`后紧接着调用`RevokeDragDrop`就出现内存泄漏,这很不寻常。因为这种使用模式已被包括Windows自身许多组件在内的数千个应用程序广泛采用。如果该模式本身存在固有的内存泄漏问题,理应早已被发现报告。
我怀疑他们的示例程序存在特殊情况。
此前我曾提到过:不,真的,你需要将所有未处理的消息传递给DefWindowProc (https://devblogs.microsoft.com/oldnewthing/20060425-16/?p=31413)。而这正是问题根源所在。
通过调试时间旅行跟踪记录,确认他们确实调用了`RegisterDragDrop`,随后也调用了`RevokeDragDrop`。但情况更为复杂:当窗口收到`WM_DESTROY`消息时,会清理所有状态。对于`WM_DESTROY`之后到达的任何消息,窗口过程会尝试访问其特定状态,但因状态不存在而放弃处理,直接返回0而不将消息传递给`DefWindowProc`。
这可就糟了。
当窗口过程无法确定如何处理消息时,应当将所有消息传递给`DefWindowProc`。在此场景下这一点尤为重要,因为部分消息属于清理消息,而清理消息的功能之一正是释放仍残留的少量内存片段。
**补充说明**:但如果我注册了一个拖放目标,随后又注销它,注销操作难道不应释放注册时分配的所有内存吗?
并没有强制性规定要求注册某物后再注销就必须立即释放与该注册相关的所有内存。系统允许缓存其认为将来会再次使用的内容。
此案例中发生的情况是:`RegisterDragDrop`函数使用的基础设施由多个组件共享。该基础设施会在首次被需要时创建并附加到窗口,最终在窗口销毁时进行清理。内存并未泄漏,只是缓存在窗口上等待后续操作使用,且该缓存会在窗口销毁时被清除。
但系统假定你必须给`DefWindowProc`机会来执行此清理操作。
### 分类
### 主题
## 作者
雷蒙德·陈(Raymond Chen)
雷蒙德参与Windows的演进发展已超过30年。2003年,他创建了名为"The Old New Thing"的网站,其受欢迎程度远超他最狂野的想象——这种发展态势至今仍令他感到不安。该网站催生了一本同名书籍(巧合的是,书名也叫The Old New Thing,Addison Wesley出版社2007年出版)。他偶尔会通过Windows Dev Docs的Twitter账号讲述些无实用信息的故事。
相似文章
一个未正式卸载却从内存中消失的DLL案例,第二部分
Ray Chen的一篇技术博文,探究一个内存损坏bug:单个字节0x01破坏HMODULE句柄,导致DLL被错误释放,进而在进程终止时崩溃。
关于滥用Windows窗口类额外字节的兼容性说明
Raymond Chen 讨论了一个历史性的 Windows 兼容性问题,其中一些 16 位程序滥用窗口类额外字节来存储私有数据,以及微软如何在保持向后兼容性的同时,对 32 位和 64 位程序堵住了这个漏洞。
为什么你说COM STA线程必须泵送消息,而我看到示例代码创建STA线程却没有泵送消息?
Raymond Chen解释说,COM STA线程仅在空闲时才需要泵送消息;一直忙碌的代码不需要显式的消息循环,但COM仍然会创建一个隐藏窗口,当线程变为空闲时需要泵送消息以避免阻塞窗口广播。
在C++/WinRT中创建敏捷版的Windows Runtime委托,第6部分
系列文章第6部分:在C++/WinRT中创建敏捷的Windows Runtime委托,修复std::unique_ptr自定义删除器构造函数可能引发的异常问题。
在C++/WinRT中创建Windows运行时代理的敏捷版本,第3部分
这篇博客文章讨论了在C++/WinRT中处理实现INoMarshal接口的Windows运行时代理,提供了一个敏捷的代理包装器,通过检查调用上下文来避免封送错误。