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

时间:2012-09-26 21:14:59

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

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

问题:

我有一个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(继承),NodeEditPanel(继承),Button,FlowLayoutPanel,Label,&gt;面板,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<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组件存在任何已知问题,请在此处列出我的每个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种类型具有直接由委托生根的实例。这可能表示代理未被正确删除。 请查看以下类型以获取更多信息。

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

  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个重复) bytes),System.EventHandler(6套,3,992个重复字节),DialogueEditor.Choice [](6套,3,928个重复字节),(...)

1 个答案:

答案 0 :(得分:20)

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();
}