You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 18:48:26