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

C#中自定义FrameWorkElementAutomationPeer实现IExpandCollapseProvider引发内存泄漏

解决WPF自定义AutomationPeer导致的内存泄漏问题

看来你遇到的是UI自动化场景下典型的COM互操作引用泄漏问题——非托管的UI自动化框架通过RefCounted handle持有了你的TreeItemAutomationPeer实例,而Peer又通过强引用绑定着TreeItem,最终导致GC无法回收这些对象。结合你的代码和DotMemory的检测结果,我给你几个针对性的解决方案:

1. 将AutomationPeer中的强引用替换为弱引用

你的TreeItemAutomationPeer里用了private readonly TreeItem _treeItem;这种强引用,这是核心泄漏点之一。改成弱引用后,即使Peer被非托管代码持有,只要TreeItem没有其他业务强引用,GC就能正常回收它。修改后的代码示例:

public class TreeItemAutomationPeer : FrameworkElementAutomationPeer, ISelectionItemProvider, IExpandCollapseProvider, IDisposable
{
    private readonly WeakReference<TreeItem> _treeItemWeakRef;
    private bool _disposed = false;

    public TreeItemAutomationPeer(TreeItem treeItem) : base(treeItem)
    {
        _treeItemWeakRef = new WeakReference<TreeItem>(treeItem);
    }

    // 辅助方法:安全获取TreeItem实例,同时检查是否已被回收
    private TreeItem GetTreeItem()
    {
        if (_disposed) return null;
        _treeItemWeakRef.TryGetTarget(out var treeItem);
        return treeItem;
    }

    public override object GetPattern(PatternInterface patternInterface)
    {
        var treeItem = GetTreeItem();
        if (treeItem == null) return base.GetPattern(patternInterface);
        
        if (patternInterface == PatternInterface.SelectionItem || patternInterface == PatternInterface.ExpandCollapse)
            return this;
        return base.GetPattern(patternInterface);
    }

    #region ISelectionItemProvider 实现(全部改为通过GetTreeItem()访问)
    public IRawElementProviderSimple SelectionContainer
    {
        get 
        {
            var treeItem = GetTreeItem();
            return treeItem?.SelectionContainer; 
        }
    }

    public bool IsSelected
    {
        get 
        {
            var treeItem = GetTreeItem();
            return treeItem?.Item.IsSelected ?? false; 
        }
    }

    public void AddToSelection()
    {
        var treeItem = GetTreeItem();
        if (treeItem == null) return;
        
        treeItem.Item.IsSelected = true;
        ItemHelper.SelectItem(treeItem, treeItem.Item);
    }

    // RemoveFromSelection、Select方法同理修改,先检查TreeItem是否存活
    #endregion

    #region IExpandCollapseProvider 实现(全部改为通过GetTreeItem()访问)
    public ExpandCollapseState ExpandCollapseState
    {
        get 
        {
            var treeItem = GetTreeItem();
            return treeItem?.Item.IsExpanded ?? ExpandCollapseState.Collapsed; 
        }
    }

    public void Expand()
    {
        var treeItem = GetTreeItem();
        if (treeItem != null)
            treeItem.Item.IsExpanded = true;
    }

    // Collapse方法同理修改
    #endregion

    #region IDisposable 实现:主动清理引用
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        
        if (disposing)
        {
            // 清空弱引用目标,加速GC回收
            _treeItemWeakRef.SetTarget(null);
        }
        
        _disposed = true;
    }

    ~TreeItemAutomationPeer()
    {
        Dispose(false);
    }
    #endregion

    protected override List<AutomationPeer> GetChildrenCore()
    {
        return null; // 保留你现有的实现,避免子元素泄漏
    }
}

同时修改TreeItem类,在创建新Peer时释放旧实例,并在Unloaded事件中主动清理:

protected override AutomationPeer OnCreateAutomationPeer()
{
    // 先释放旧的Peer实例
    _itemAutomationPeer?.Dispose();
    _itemAutomationPeer = new TreeItemAutomationPeer(this);
    return _itemAutomationPeer;
}

public TreeItem()
{
    Unloaded += TreeItem_Unloaded;
}

private void TreeItem_Unloaded(object sender, RoutedEventArgs e)
{
    // 控件卸载时主动释放Peer
    _itemAutomationPeer?.Dispose();
    _itemAutomationPeer = null;
}

2. 客户端(控制台应用)必须显式释放COM对象

控制台作为UI自动化客户端,获取的AutomationElement和对应的Pattern对象(比如ExpandCollapsePattern)都是COM包装器,必须手动释放才能让非托管端的引用计数归零。示例清理逻辑:

// 客户端的资源清理方法
public void CleanupAutomationResources()
{
    // 先释放获取到的Pattern对象
    if (_expandCollapsePattern != null)
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(_expandCollapsePattern);
        _expandCollapsePattern = null;
    }
    
    if (_selectionItemPattern != null)
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(_selectionItemPattern);
        _selectionItemPattern = null;
    }
    
    // 释放AutomationElement的缓存数据
    if (_targetTreeItemElement != null)
    {
        _targetTreeItemElement.ReleaseCachedData();
        _targetTreeItemElement = null;
    }
    
    // 测试场景下可以强制触发GC,加速回收
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

重点提醒:只要客户端通过AutomationElement.GetCurrentPattern()获取了Pattern实例,就必须调用Marshal.ReleaseComObject(),否则非托管端的引用计数永远不会归零,泄漏会持续存在。

3. 额外优化建议

  • 如果你后续需要支持子元素的自动化,不要让GetChildrenCore()返回null,而是按需创建子Peer并同样使用弱引用,避免缓存子Peer导致的连锁泄漏。
  • 检查TreeItem中_container的引用类型,如果是强引用,考虑是否也需要改成弱引用,避免引发新的泄漏点。

内容的提问来源于stack exchange,提问作者Whiletrue

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 09:06:53