【问题标题】:Properly disposing of, and removing references to UserControls, to avoid memory leak正确处理和删除对 UserControls 的引用,以避免内存泄漏
【发布时间】:2012-09-26 21:14:58
【问题描述】:

我正在使用 Visual c# express 2010 在 c# 中开发一个 Windows 窗体应用程序 (.NET 4.0)。我无法释放分配给我不再使用的 UserControls 的内存。

问题:

我有一个 FlowLayoutPanel,其中显示了自定义用户控件。 FlowLayoutPanel显示搜索结果等,所以显示的UserControls集合必须反复更新。

在创建和显示每组新的 UserControl 之前,对当前包含在我的 FlowLayoutPanel 的 ControlCollection(Controls 属性)中的所有控件调用 Dispose(),然后在同一个 ControlCollection 上调用 Clear()。

这似乎不足以处理 UserControls 使用的资源,因为每组新的 UserControls 创建并添加到我的 ControlCollection 中,我的 UserControls 似乎也没有被垃圾回收声明. 应用程序的内存使用量在短时间内急剧攀升,然后达到一个平台,直到我显示另一个列表。我还使用.NET Memory Profiler 分析了我的应用程序,它报告了许多可能的内存泄漏(请参阅下面的部分。)

我认为出了什么问题:

我错了。 问题是由于使用 foreach 构造迭代 ControlCollection 并在其控件上调用 Dispose() 导致的错误,Hans Passant 在他的回答中对此进行了描述。


这个问题似乎是由我的 UserControls 中使用的 ToolTip 引起的。当我删除这些时,我的 UserControls 似乎被垃圾收集所占用。 .NET 内存分析器证实了这一点。我之前的测试中的问题 1 和 6(见下部分)不再出现​​,它报告了一个新问题:

未部署的实例(释放资源并删除外部引用) 7 种类型的实例已被垃圾收集而没有正确处理。 调查以下类型以获取更多信息。

ChoiceEditPanel(继承)、NodeEditPanel(继承)、Button、FlowLayoutPanel、Label、>Panel、TextBox

即使工具提示的参考消失了,这不是一个长期的解决方案,当我不再需要它们时,仍然存在确定性地处置我的用户控件的问题。但是,它不如删除对工具提示的引用重要。

代码和更多细节

我使用一个名为 NodesDisplayPanel 的 UserControl,它充当 FlowLayoutPanel 的包装器。这是我的 NodesDisplayPanel 类中的方法,用于从我的 FlowLayoutPanel 中清除所有控件:

public void Clear() {
    foreach (Control control in flowPanel.Controls) {
        if (control != NodeEditPanel.RootNodePanel) {
            control.Dispose();
        }
    }
    flowPanel.Controls.Clear();
    // widthGuide is used to control the widths of the Controls below it,
    // which have Dock set to Dockstyle.Top
    widthGuide = new Panel();
    widthGuide.Location = new Point(0, 0);
    widthGuide.Margin = new Padding(0);
    widthGuide.Name = "widthGuide";
    widthGuide.Size = new Size(809, 1);
    widthGuide.TabIndex = 0;
    flowPanel.Controls.Add(widthGuide);
}

这些方法用于添加控件:

public void AddControl(Control control) {
    flowPanel.Controls.Add(control);
}
public void AddControls(Control[] controls) {
    flowPanel.Controls.AddRange(controls);
}

这是实例化新 NodeEditPanel 并通过我的 NodesDisplayPanel 将它们添加到我的 FlowLayoutPanel 的方法。此方法来自 ListNodesPanel(如下面的屏幕截图所示),它是实例化和添加 NodeEditPanel 的几个 UserControl 之一:

public void UpdateNodesList() {
    Node[] nodes = Data.Instance.Nodes;
    Array.Sort(nodes,(IComparer<Node>) comparers[orderByDropDownList.SelectedIndex]);
    if ((listDropDownList.SelectedIndex == 1)
        && (nodes.Length > numberOfNodesNumUpDown.Value)) {
        Array.Resize(ref nodes,(int) numberOfNodesNumUpDown.Value);
    }
    NodeEditPanel[] nodePanels = new NodeEditPanel[nodes.Length];
    for (int index = 0; index < nodes.Length; index ++) {
        nodePanels[index] = new NodeEditPanel(nodes[index]);
    }
    nodesDisplayPanel.Clear();
    nodesDisplayPanel.AddControls(nodePanels);
}

这是我的 ListNodesPanel UserControl 的自定义初始化方法。希望它能让 UpdateNodesList() 方法更清晰:

private void NonDesignerInnitialisation() {
    this.Dock = DockStyle.Fill;
    listDropDownList.SelectedIndex = 0;
    orderByDropDownList.SelectedIndex = 0;
    numberOfNodesNumUpDown.Enabled = false;
    comparers = new IComparer<Node>[3];
    comparers[0] = new CompareNodesByID();
    comparers[1] = new CompareNodesByNPCText();
    comparers[2] = new CompareNodesByChoiceCount();
}

如果特定 Windows.Forms 组件存在任何已知问题,以下是我的每个用户控件中使用的所有组件类型的列表:

选择编辑面板:

  • 面板
  • 标签
  • 按钮
  • 文本框
  • 工具提示

节点编辑面板

  • 选择编辑面板
  • 流布局面板
  • 面板
  • 标签
  • 按钮
  • 文本框
  • 工具提示

我还在为一些文本框使用i00SpellCheck

.NET Memory Profiler 最初报告的可能问题:

我让我的应用程序显示 50 个左右的 NodeEditPanel,两次,第二个列表与第一个列表具有相同的值,但是是不同的实例。 .Net Memory Profiler 比较了应用程序在第一次和第二次操作后的状态,并生成了以下可能问题列表:

  1. 直接事件处理程序根
    一种类型具有直接由 EventHandler 根植的实例。这可能表明 EventHandler 没有被正确删除。 调查以下类型以获取更多信息。

    工具提示

  2. 已处理的实例
    2 种类型的实例已被处置但未 GC。 调查以下类型以获取更多信息。

    System.Drawing.Graphics、WindowsFont

  3. 未部署的实例(释放资源)
    6 种类型的实例已被垃圾收集而没有正确处理。 调查以下类型以获取更多信息。

    System.Drawing.Bitmap、System.Drawing.Font、System.Drawing.Region、Control.FontHandleWrapper、光标、WindowsFont

  4. 直接代表根
    2 种类型具有直接由委托创建的实例。这可能表明委托没有被正确删除。 调查以下类型以获取更多信息。

    系统.__过滤器,__过滤器

  5. 固定实例
    2 种类型具有固定在内存中的实例。 调查以下类型以获取更多信息。

    System.Object, System.Object[]

  6. 间接事件处理程序根
    53 种类型具有由 EventHandler 间接根植的实例。这可能表明 EventHandler 没有被正确删除。 调查以下类型以获取更多信息。

    , ChoiceEditPanel, NodeEditPanel, ArrayList, Hashtable, Hashtable.bucket[], Hashtable.KeyCollection, Container, Container.Site, EventHandlerList, (...)

  7. 未部署的实例(内存/资源利用率)
    3 种类型的实例已被垃圾收集而没有正确处理。 调查以下类型以获取更多信息。

    System.IO.BinaryReader、System.IO.MemoryStream、UnmanagedMemoryStream

  8. 重复实例
    71 种类型有重复实例(492 组,741,229 个重复字节)。重复的实例会导致不必要的内存消耗。 调查以下类型以获取更多信息。

    GPStream(8 组,318,540 个重复字节),PropertyStore.IntegerEntry[](24 组,93,092 个重复字节),PropertyStore(10 组,53,312 个重复字节),PropertyStore.SizeWrapper(16 组,41,232 个重复字节),PropertyStore .PaddingWrapper(8 套,38,724 个重复字节),PropertyStore.RectangleWrapper(28 套,32,352 个重复字节),PropertyStore.ColorWrapper(13 套,30,216 个重复字节),System.Byte[](3 套,25,622 个重复字节),ToolTip .TipInfo(10组,21056个重复字节),Hashtable(2组,20148个重复字节),(...)

  9. 空弱引用
    WeakReference 类型的实例不再存在。 调查 WeakReference 类型以获取更多信息。

    System.WeakReference

  10. 未处理的实例(明确引用)
    一种类型的实例已被垃圾收集而没有正确处理。 调查以下类型以获取更多信息。

    事件处理程序列表

  11. 大型实例
    2 种类型的实例位于大对象堆中。 调查以下类型以获取更多信息。

    Dictionary.DictionaryItem[], System.Object[]

  12. 持有重复的实例
    25 种类型具有由其他重复实例持有的重复实例(136 组,371,766 个重复字节)。 调查以下类型以获取更多信息。

    System.IO.MemoryStream(8 组,305,340 个重复字节),System.Byte[](7 组,248,190 个重复字节),PropertyStore.ObjectEntry[](10 组,40,616 个重复字节),Hashtable.bucket[] (2组,9696个重复字节),System.String(56组,8482个重复字节),EventHandlerList.ListEntry(6组,4072个重复字节),List(6组,4072个重复字节),EventHandlerList(3组,3992个重复字节) bytes), System.EventHandler (6 组, 3,992 重复字节), DialogueEditor.Choice[] (6 组, 3,928 重复字节), (...)

【问题讨论】:

  • 没有必要将事件注销到自身或子控件(如 button.click)。 UserControl(或任何对象)注册到具有更长寿命的外部对象时的正常泄漏情况。结果是该对象的事件表指向 UserControl... 并使其保持活动状态。 (例如,如果您的 UC 注册了它的父对话框的“关闭”或其他什么。)
  • 我怀疑您额外的 Dispose 代码毫无意义,但会显示更多代码...例如,显示控件如何/何时实例化并添加到 FlowPanel 的代码在哪里?
  • 我更新了我的问题,添加了更多代码和其他信息。 ToolTip 似乎是保持对我的 UserControls 的引用的对象。我不知道为什么。可能是因为它的 Draw 事件是由操作系统处理的
  • Dispose 与托管内存使用无关。这是关于处置非托管资源,如数据库连接。
  • 这个问题已经变得很大了。既然您已经了解了更多,也许您应该在一个新问题中缩小范围?

标签: c# winforms visual-studio-2010 .net-4.0 visual-c#-express-2010


【解决方案1】:
foreach (Control control in flowPanel.Controls) {
    if (control != NodeEditPanel.RootNodePanel) {
        control.Dispose();
    }
}
flowPanel.Controls.Clear();

这是一个非常经典的 Winforms bug,很多程序员都被它咬过。释放控件还会将其从父控件集合中移除。大多数 .NET 集合类在迭代它们更改集合时会触发 InvalidOperationException,但 ControlCollection 类没有这样做。效果是您的 foreach 循环跳过元素,它只处理偶数控件。

您已经发现了问题,但通过调用 Controls.Clear() 使问题变得更糟。特别讨厌,因为垃圾收集器不会最终确定以这种方式删除的控件。创建控件的本机窗口句柄后,它将保持由将窗口句柄映射到控件的内部表引用。只有销毁本机窗口才会从该表中删除引用。这种情况在这样的代码中永远不会发生,调用 Dispose() 是一个非常困难的要求。在 .NET 中非常不寻常。

解决方案是向后迭代 Controls 集合,以便处理控件不会影响您迭代的内容。像这样:

for (int ix = flowPanel.Controls.Count-1; ix >= 0; --ix) {
    var ctl = flowPanel.Controls[ix];
    if (ctl != NodeEditPanel.RootNodePanel) ctl.Dispose();
}

【讨论】:

  • 我希望我能在几周前阅读这个答案。我一直在使用这种方法来删除/处理控件,这让我发疯。我搬到for(int i ... i++) 循环来完成工作。现在我知道我没有疯。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-02-12
  • 2012-11-30
  • 1970-01-01
  • 2019-06-06
  • 1970-01-01
  • 2013-01-23
相关资源
最近更新 更多