Properly disposing of, and removing references to UserControls, to avoid memory leak

房东的猫 提交于 2019-12-17 15:59:11

问题


I'm developing a Windows Forms application (.NET 4.0) in c# using Visual c# express 2010. I'm having trouble freeing up memory allocated to UserControls I'm no-longer using.

The problem:

I have a FlowLayoutPanel, where custom UserControls are displayed. The FlowLayoutPanel displays search results and so on, so the collection of UserControls that are displayed must be repeatedly updated.

Before every new set of these UserControls are created and displayed, Dispose() is called on all the Controls currently contained in my FlowLayoutPanel's ControlCollection (Controls property), then Clear() is called on the same ControlCollection.

This doesn't seem to be sufficient to dispose of the resources used by the UserControls because with each new set of UserControls which is created and added to my ControlCollection, nor do my UserControls seem to be claimed by garbage collection. The application's memory usage climbs sharply over a short period of time, then reaches a plateau until I display another list. I've also analysed my application with .NET Memory Profiler, which reports a number of possible memory leaks (See lower section.)

What I think is going wrong:

I was wrong. The problem was a bug caused by using a foreach construct to iterate through a ControlCollection and calling Dispose() on its controls, which Hans Passant describes in his answer.


The problem seems to be caused by ToolTip used in my UserControls. When I removed these, my UserControls appeared to be claimed by garbage collection. This was confirmed by .NET memory profiler. Problem 1 and 6 from my earlier test (see lower section) no longer appeared and it reported a new problem:

Undisposed instances (release resource and remove external references) 7 types have instances that have been garbage collected without being properly disposed. Investigate the types below for more information.

ChoiceEditPanel (inherited), NodeEditPanel (inherited), Button, FlowLayoutPanel, Label, > Panel, TextBox

Even with the ToolTip's reference gone, which isn't a long-term solution, there is still the problem of deterministically disposing of my UserControls when I no longer need them. However, isn't as important as removing the references to the ToolTips.

Code and more details

I use a UserControl called NodesDisplayPanel which acts as a wrapper to a FlowLayoutPanel. Here is the method in my NodesDisplayPanel class which is used to clear all Controls from my 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);
}

These methods are used to add Controls:

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

Here is the method that instantiates new NodeEditPanels and adds them to my FlowLayoutPanel, via my NodesDisplayPanel. This method is from ListNodesPanel (as seen in screenshot below), one of several UserControls that instantiate and add NodeEditPanels:

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

This is my custom innitilization method for my ListNodesPanel UserControl. Hopefully it will make the UpdateNodesList() method a bit clearer:

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

In case there are any known issues with particular Windows.Forms Components, Here's a list of all the types of Components that are used in each of my UserControls:

ChoiceEditPanel:

  • Panel
  • Label
  • Button
  • TextBox
  • ToolTip

NodeEditPanel

  • ChoiceEditPanel
  • FlowLayoutPanel
  • Panel
  • Label
  • Button
  • Textbox
  • ToolTip

I am also using the i00SpellCheck library for some of the TextBoxes

Possible issues initially reported by .NET Memory Profiler:

I got my application to display 50 or so NodeEditPanels, twice, the second list having identical values to the first but being different instances. .Net Memory Profiler compared the states of the application after the first and second operation, and produced this list of possible problems:

  1. Direct EventHandler roots
    One type has instances that are directly rooted by an EventHandler. This can indicate that an EventHandler has not been properly removed. Investigate the type below for more information.

    ToolTip

  2. Disposed instances
    2 types have instances that have been disposed but not GCed. Investigate the types below for more information.

    System.Drawing.Graphics, WindowsFont

  3. Undisposed instances (release resource)
    6 types have instances that have been garbage collected without being properly disposed. Investigate the types below for more information.

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

  4. Direct delegate roots
    2 types have instances that are directly rooted by a delegate. This can indicate that the delegate has not been properly removed. Investigate the types below for more information.

    System.__Filters, __Filters

  5. Pinned instances
    2 types have instances that are pinned in memory. Investigate the types below for more information.

    System.Object, System.Object[]

  6. Indirect EventHandler roots
    53 types have instances that are indirectly rooted by an EventHandler. This can indicate that the EventHandler has not been properly removed. Investigate the types below for more information.

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

  7. Undisposed instances (memory/resource utilization)
    3 types have instances that have been garbage collected without being properly disposed. Investigate the types below for more information.

    System.IO.BinaryReader, System.IO.MemoryStream, UnmanagedMemoryStream

  8. Duplicate instances
    71 types have duplicate instances (492 sets, 741,229 duplicated bytes). Duplicate instances can cause unnecessary memory consumption. Investigate the types below for more information.

    GPStream (8 sets, 318,540 duplicated bytes), PropertyStore.IntegerEntry[] (24 sets, 93,092 duplicated bytes), PropertyStore (10 sets, 53,312 duplicated bytes), PropertyStore.SizeWrapper (16 sets, 41,232 duplicated bytes), PropertyStore.PaddingWrapper (8 sets, 38,724 duplicated bytes), PropertyStore.RectangleWrapper (28 sets, 32,352 duplicated bytes), PropertyStore.ColorWrapper (13 sets, 30,216 duplicated bytes), System.Byte[] (3 sets, 25,622 duplicated bytes), ToolTip.TipInfo (10 sets, 21,056 duplicated bytes), Hashtable (2 sets, 20,148 duplicated bytes), (...)

  9. Empty weak reference
    The WeakReference type has instances that are no longer alive. Investigate the WeakReference type for more information.

    System.WeakReference

  10. Undisposed instances (clear references)
    One type has instances that have been garbage collected without being properly disposed. Investigate the type below for more information.

    EventHandlerList

  11. Large instances
    2 types have instances that are located in the large object heap. Investigate the types below for more information.

    Dictionary.DictionaryItem[], System.Object[]

  12. Held duplicate instances
    25 types have duplicate instances that are held by other duplicate instances (136 sets, 371,766 duplicated bytes). Investigate the types below for more information.

    System.IO.MemoryStream (8 sets, 305,340 duplicated bytes), System.Byte[] (7 sets, 248,190 duplicated bytes), PropertyStore.ObjectEntry[] (10 sets, 40,616 duplicated bytes), Hashtable.bucket[] (2 sets, 9,696 duplicated bytes), System.String (56 sets, 8,482 duplicated bytes), EventHandlerList.ListEntry (6 sets, 4,072 duplicated bytes), List (6 sets, 4,072 duplicated bytes), EventHandlerList (3 sets, 3,992 duplicated bytes), System.EventHandler (6 sets, 3,992 duplicated bytes), DialogueEditor.Choice[] (6 sets, 3,928 duplicated bytes), (...)


回答1:


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

This is a pretty classic Winforms bug, many programmers have been bitten by it. Disposing a control also removes it from the parent's Control collection. Most .NET collection classes trigger an InvalidOperationException when iterating them changes the collection but that wasn't done for the ControlCollection class. The effect is that your foreach loop skips elements, it only disposes the even-numbered controls.

You already discovered the problem, but made it considerably worse by calling Controls.Clear(). Extra-specially nasty because the garbage collector will not finalize the controls that are removed that way. After the native window handle for a control is created, it will stay referenced by an internal table that maps Window handles to controls. Only destroying the native window removes the reference from that table. That never happens in code like this, calling Dispose() is a rock hard requirement. Very unusual in .NET.

The solution is to iterate the Controls collection backwards so that disposing controls doesn't affect what you iterate. Like this:

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


来源:https://stackoverflow.com/questions/12610535/properly-disposing-of-and-removing-references-to-usercontrols-to-avoid-memory

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!