15

我正在使用 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 组件存在任何已知问题,以下是我的每个用户控件中使用的所有组件类型的列表:

选择编辑面板:

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

节点编辑面板

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

我还在为一些 TextBoxes使用i00SpellCheck库

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

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

  1. 直接 EventHandler 根
    一种类型具有直接由 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 类型具有固定在内存中的实例。调查以下类型以获取更多信息。

    系统对象,系统对象 []

  6. 间接事件处理程序根
    53 种类型具有由事件处理程序间接根的实例。这可能表明 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. 空弱引用
    Wea​​kReference 类型的实例不再存在。调查 WeakReference 类型以获取更多信息。

    System.WeakReference

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

    事件处理程序列表

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

    Dictionary.DictionaryItem[], System.Object[]

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

    System.IO.MemoryStream(8组,305340个重复字节),System.Byte[](7组,248190个重复字节),PropertyStore.ObjectEntry[](10组,40616个重复字节),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 个重复字节),(...)

4

1 回答 1

23
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();
}
于 2012-09-27T21:19:36.380 回答