15

Visual c#express 2010を使用してc#でWindowsフォームアプリケーション(.NET 4.0)を開発しています。使用しなくなったUserControlsに割り当てられたメモリを解放するのに問題があります。

問題:

カスタムUserControlsが表示されるFlowLayoutPanelがあります。FlowLayoutPanelは検索結果などを表示するため、表示されるUserControlのコレクションを繰り返し更新する必要があります。

これらのUserControlのすべての新しいセットが作成および表示される前に、FlowLayoutPanelのControlCollection(Controlsプロパティ)に現在含まれているすべてのコントロールでDispose()が呼び出され、次に同じControlCollectionでClear()が呼び出されます。

これは、UserControlsによって使用されるリソースを破棄するのに十分ではないようです。これは、作成されてControlCollectionに追加されるUserControlsの新しいセットごとに、UserControlsがガベージコレクションによって要求されていないように見えるためです。アプリケーションのメモリ使用量は、短期間に急激に増加し、その後、別のリストを表示するまでプラトーに達します。また、 .NETメモリプロファイラーを使用してアプリケーションを分析しました。これは、考えられるメモリリークの数を報告します(下のセクションを参照)。

私が間違っていると思うこと:

私は間違っていた。問題は、foreachコンストラクトを使用してControlCollectionを反復処理し、そのコントロールでDispose()を呼び出すことによって引き起こされたバグでした。これは、HansPassantが回答で説明しています。


この問題は、UserControlsで使用されているToolTipが原因のようです。これらを削除すると、UserControlsがガベージコレクションによって要求されているように見えました。これは、.NETメモリプロファイラーによって確認されました。以前のテスト(下のセクションを参照)の問題1と6は表示されなくなり、新しい問題が報告されました。

破棄されないインスタンス(リソースを解放し、外部参照を削除する)7つのタイプには、適切に破棄されずにガベージコレクションされたインスタンスがあります。詳細については、以下のタイプを調べてください。

ChoiceEditPanel(継承)、NodeEditPanel(継承)、Button、FlowLayoutPanel、Label、> Panel、TextBox

長期的な解決策ではないToolTipの参照がなくなったとしても、UserControlが不要になったときに決定論的に破棄するという問題があります。ただし、ツールチップへの参照を削除するほど重要ではありません。

コードと詳細

FlowLayoutPanelのラッパーとして機能するNodesDisplayPanelと呼ばれるUserControlを使用します。FlowLayoutPanelからすべてのコントロールをクリアするために使用されるNodesDisplayPanelクラスのメソッドは次のとおりです。

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に追加するメソッドです。このメソッドは、NodeEditPanelsをインスタンス化して追加するいくつかのUserControlの1つであるListNodesPanel(下のスクリーンショットを参照)からのものです。

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

これは、ListNodesPanelUserControlのカスタム初期化メソッドです。うまくいけば、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
  • パネル
  • ラベル
  • ボタン
  • テキストボックス
  • ツールチップ

一部のTextBoxにはi00SpellCheckライブラリも使用しています

.NETメモリプロファイラによって最初に報告された可能性のある問題:

アプリケーションに50個ほどのNodeEditPanelを2回表示させました。2番目のリストは、最初のリストと同じ値ですが、インスタンスが異なります。.Net Memory Profilerは、1回目と2回目の操作後のアプリケーションの状態を比較し、考えられる問題のリストを作成しました。

  1. 直接EventHandlerルート
    1つのタイプには、EventHandlerによって直接ルートされるインスタンスがあります。これは、EventHandlerが適切に削除されていないことを示している可能性があります。詳細については、以下のタイプを調べてください。

    ツールチップ

  2. 破棄されたインスタンス
    2タイプには、破棄されたがGCされていないインスタンスがあります。詳細については、以下のタイプを調べてください。

    System.Drawing.Graphics、WindowsFont

  3. 廃棄されていないインスタンス(リソースの解放)
    6種類には、適切に廃棄されずにガベージコレクションされたインスタンスがあります。詳細については、以下のタイプを調べてください。

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

  4. 直接デリゲートルート
    2タイプには、デリゲートによって直接ルートされるインスタンスがあります。これは、デリゲートが適切に削除されていないことを示している可能性があります。詳細については、以下のタイプを調べてください。

    System .__ Filters、__ Filters

  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重複バイト)、ハッシュテーブル(2セット、20,148重複バイト)、(...)

  9. 空の弱参照
    WeakReferenceタイプには、もう生きていないインスタンスがあります。詳細については、WeakReferenceタイプを調べてください。

    System.WeakReference

  10. 破棄されていないインスタンス(明確な参照)
    1つのタイプには、適切に破棄されずにガベージコレクションされたインスタンスがあります。詳細については、以下のタイプを調べてください。

    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重複バイト)、(...)

4

1 に答える 1

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