问题
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:
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
Disposed instances
2 types have instances that have been disposed but not GCed. Investigate the types below for more information.System.Drawing.Graphics, WindowsFont
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
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
Pinned instances
2 types have instances that are pinned in memory. Investigate the types below for more information.System.Object, System.Object[]
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, (...)
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
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), (...)
Empty weak reference
The WeakReference type has instances that are no longer alive. Investigate the WeakReference type for more information.System.WeakReference
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
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[]
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