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
相关产品推荐
相关产品推荐

