正确处理和删除对UserControls的引用,以避免内存泄漏

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

问题:

我有一个FlowLayoutPanel,显示自定义UserControls。 FlowLayoutPanel显示搜索结果等,因此必须重复更新显示的UserControl集合。

在创建和显示每个新的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(inheritance),NodeEditPanel(inheritance),Button,FlowLayoutPanel,Label,> Panel,TextBox

即使工具提示的参考已经消失,这不是一个长期解决方案,但当我不再需要时,仍然存在确定性地处理我的UserControls的问题。 但是,删除对工具提示的引用并不重要。

代码和更多细节

我使用一个名为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); } 

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

 public void UpdateNodesList() { Node[] nodes = Data.Instance.Nodes; Array.Sort(nodes,(IComparer) 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[3]; comparers[0] = new CompareNodesByID(); comparers[1] = new CompareNodesByNPCText(); comparers[2] = new CompareNodesByChoiceCount(); } 

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

ChoiceEditPanel:

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

NodeEditPanel

  • ChoiceEditPanel
  • FlowLayoutPanel的
  • 面板
  • 标签
  • 按键
  • 文本框
  • 工具提示

我也在使用i00SpellCheck库来处理一些TextBox

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

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

  1. 直接EventHandler根
    一种类型具有直接由EventHandler生根的实例。 这可能表示尚未正确删除EventHandler。 请查看以下类型以获取更多信息。

    工具提示

  2. 处置实例
    2种类型具有已处置但未GCed的实例。 请查看以下类型以获取更多信息。

    System.Drawing.Graphics,WindowsFont

  3. 未分配的实例(释放资源)
    6种类型的实例在没有正确处理的情况下被垃圾收集。 请查看以下类型以获取更多信息。

    System.Drawing.Bitmap,System.Drawing.Font,System.Drawing.Region,Control.FontHandleWrapper,Cursor,WindowsFont

  4. 直接代表根
    2种类型具有直接由委托生根的实例。 这可能表示代理未被正确删除。 请查看以下类型以获取更多信息。

    System .__filter,__filter

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

    System.Object,System.Object []

  6. 间接EventHandler根
    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组,21,056个重复字节),Hashtable(2组,20,148个重复字节),(…)

  9. 空弱参考
    WeakReference类型具有不再存活的实例。 调查WeakReference类型以获取更多信息。

    System.WeakReference

  10. 未公开的实例(明确的引用)
    一种类型的实例在没有正确处理的情况下被垃圾收集。 请查看以下类型以获取更多信息。

    EventHandlerList

  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套,9,696个重复字节),System.String(56个集,8,482个重复字节),EventHandlerList.ListEntry(6个集,4,072个重复字节),List(6个集,4,072个重复字节),EventHandlerList(3个集,3,992个重复字节), System.EventHandler(6组,3,992个重复字节),DialogueEditor.Choice [](6组,3,928个重复字节),(…)

 foreach (Control control in flowPanel.Controls) { if (control != NodeEditPanel.RootNodePanel) { control.Dispose(); } } flowPanel.Controls.Clear(); 

这是一个非常经典的Winforms错误,很多程序员都被它咬了。 处置控件也会将其从父级的Control集合中删除。 大多数.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(); }