WPF中HwndHost重复创建销毁引发内存泄漏问题排查
WPF HwndHost持续创建场景内存泄漏修复
根因定位
- Dispose执行时序错误:自定义
MediaFoundationVideoHost重写的Dispose(bool disposing)方法中,提前将hwndHost、handleRef、parentHandle等句柄字段置为IntPtr.Zero,再调用基类Dispose方法。HwndHost基类的Dispose流程会触发DestroyWindowCore执行非托管窗口销毁,提前清空句柄会导致基类内部维护的空域映射、句柄引用计数无法正常归零,内核窗口对象残留。 - UI线程同步阻塞导致资源回收链路中断:鼠标事件中使用
while(true)同步死循环直接占用UI线程,WPF的控件卸载、布局更新、Dispatcher优先级队列中的资源清理操作全部被阻塞,即使调用了Dispose、清空了集合,ItemsControl生成的项容器、HwndHost的托管包装对象也没有机会完成卸载,随运行时间持续积压。 - WndProc实现不符合规范:重写的
WndProc对所有未处理消息直接返回IntPtr.Zero,部分窗口生命周期消息(如WM_NCDESTROY)需要走默认窗口过程完成资源清理,硬返回0会中断User32内部的GDI对象、窗口属性块的回收流程。 - P/Invoke参数传值不严谨:调用
CreateWindowEx时最后一个lpParam参数直接传入值类型0,在64位环境下会出现栈对齐偏差,可能导致窗口创建时分配的内核属性块无法关联到销毁流程。
修复方案
1. 修正HwndHost自定义实现的资源回收逻辑
调整Dispose执行顺序,先完成基类销毁再清理自有字段,修正WndProc与P/Invoke传参:
// 修正Dispose时序 protected override void Dispose(bool disposing) { lock (this.lockObj) { // 先执行基类销毁逻辑,触发DestroyWindowCore完成窗口销毁 base.Dispose(disposing); // 销毁完成后再清空自有字段引用 handleRef = new HandleRef(null, IntPtr.Zero); parentHandle = IntPtr.Zero; hwndHost = IntPtr.Zero; } } // 修正BuildWindowCore的P/Invoke参数 protected override HandleRef BuildWindowCore(HandleRef hwndParent) { this.hwndHost = IntPtr.Zero; this.parentHandle = hwndParent.Handle; int style = WS_CHILD | WS_VISIBLE | SS_BLACKFRAME | SS_WHITERECT | SS_NOTIFY; this.hwndHost = CreateWindowEx(0, "static", string.Empty, style, 0, 0, 200, 200, hwndParent.Handle, (IntPtr)HostId, IntPtr.Zero, IntPtr.Zero); // 原代码此处传0,改为IntPtr.Zero适配64位 this.handleRef = new HandleRef(this, this.hwndHost); return this.handleRef; } // 补充DestroyWindowCore的句柄有效性判断 protected override void DestroyWindowCore(HandleRef hwnd) { if (hwnd.Handle != IntPtr.Zero) { DestroyWindow(hwnd.Handle); } } // 修正WndProc逻辑,未处理消息走基类默认流程 protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) { // 自定义消息处理逻辑可在此处添加 return base.WndProc(hwnd, msg, wparam, lparam, ref handled); }
2. 替换同步阻塞循环,保证UI线程调度正常
移除while(true)死循环,改用DispatcherTimer异步执行创建销毁逻辑,给UI线程留出资源清理的调度窗口:
private DispatcherTimer _renderTimer; private void Grid_MouseDown(object sender, MouseButtonEventArgs e) { // 避免重复启动定时器 if (_renderTimer != null) return; _renderTimer = new DispatcherTimer(); _renderTimer.Interval = TimeSpan.FromMilliseconds(200); _renderTimer.Tick += (s, args) => { // 先释放旧实例 foreach (var item in Items) { item.Dispose(); } Items.Clear(); // 强制排空Dispatcher后台优先级队列,完成控件卸载 Dispatcher.Invoke(DispatcherPriority.Background, new Action(() => { })); // 创建新实例 for (int i = 0; i < 25; i++) { Items.Add(new MediaFoundationVideoHost()); } }; _renderTimer.Start(); }
3. 长期优化建议
- HwndHost属于重量级非托管资源,不建议高频创建销毁,可采用对象池复用实例,切换视频源时只更新外部播放器的渲染目标句柄即可。
- 若业务场景确实需要动态创建销毁,可每执行10~20次循环后手动触发一次二代GC回收,避免托管堆碎片化导致的内存虚高:
GC.Collect(2, GCCollectionMode.Forced, true, true);
内容的提问来源于stack exchange,提问作者Milan
相关产品推荐
相关产品推荐

