5

[更新、下部を参照してください!]

FlowDocumentReaderで WPFをホストしている WinForms アプリケーションにメモリ リークがありますElementHost。この問題を単純なプロジェクトで再現し、以下のコードを追加しました。

アプリケーションの機能

押すとbutton1

  • UserControl1のみを含むがFlowDocumentReader作成され、 に設定されElementHostますChild
  • FlowDocumentテキスト ファイルから作成されます (数千行の とFlowDocumentが含まれているだけです) 。StackPanel<TextBox/>
  • のプロパティはこれに設定されていFlowDocumentReaderますDocumentFlowDocument

この時点で、ページはFlowDocument正しくレンダリングされます。予想通り、大量のメモリが使用されます。

問題

  • をもう一度クリックするbutton1と、メモリ使用量が増加し、プロセスが繰り返されるたびに増加し続けます! 大量の新しいメモリが使用されているにもかかわらず、GC が収集されません! 次の理由により、そこにあってはならない参照はありません。

  • whichを nullbutton2に設定elementHost1.Childして GC を呼び出すと (以下のコードを参照)、別の奇妙なことが起こります。メモリはクリーンアップされませんが、数秒間クリックし続けると、最終的にメモリが解放されます!

このすべてのメモリが使用されたままになることは、私たちにとって容認できません。また、コレクションElementHostからを削除し、参照を null に設定してから GC を呼び出しても、メモリは解放されません。ControlsDisposing

私が欲しいもの

  • が複数回クリックされた場合button1、メモリ使用量が増え続けることはありません
  • すべてのメモリを解放できるはずです (これは「実際の」アプリケーションの 1 つのウィンドウにすぎず、閉じているときにこれを実行したい)

これは、メモリ使用量が問題にならないものではなく、いつでも GC に収集させることができます。実際には、マシンの速度が著しく低下します。

コード

VS プロジェクトをダウンロードしたい場合は、ここにアップロードしました: http://speedy.sh/8T5P2/WindowsFormsApplication7.zip

それ以外の場合は、関連するコードを次に示します。デザイナーでフォームに 2 つのボタンを追加し、それらをイベントに接続するだけです。Form1.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Windows.Documents;
using System.IO;
using System.Xml;
using System.Windows.Markup;
using System.Windows.Forms.Integration;


namespace WindowsFormsApplication7
{
    public partial class Form1 : Form
    {
        private ElementHost elementHost;

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            string rawXamlText = File.ReadAllText("in.txt");
            using (var flowDocumentStringReader = new StringReader(rawXamlText))
            using (var flowDocumentTextReader = new XmlTextReader(flowDocumentStringReader))
            {
                if (elementHost != null)
                {
                    Controls.Remove(elementHost);
                    elementHost.Child = null;
                    elementHost.Dispose();
                }

                var uc1 = new UserControl1();
                object document = XamlReader.Load(flowDocumentTextReader);
                var fd = document as FlowDocument;
                uc1.docReader.Document = fd;

                elementHost = new ElementHost();
                elementHost.Dock = DockStyle.Fill;
                elementHost.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
                Controls.Add(elementHost);
                elementHost.Child = uc1;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (elementHost != null)
                elementHost.Child = null;

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }
}

UserControl1.xaml

<UserControl x:Class="WindowsFormsApplication7.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <FlowDocumentReader x:Name="docReader"></FlowDocumentReader>
</UserControl>

編集:

私はついにこれに再び対処する時間ができました。私が試したのはElementHost、ボタンを押すたびに再利用する代わりに、破棄して再作成することです。これは少しは役に立ちますが、ボタン 1 をクリックするだけでメモリが上がるのではなく、スパム クリックするとメモリが上下するという意味では、それでも問題は解決しません。フォームは閉じています。だから今、私は賞金を上げています。

ここで何が問題なのか混乱しているように見えるので、リークを再現するための正確な手順は次のとおりです。

1) タスクマネージャーを開く

2) 「開始」ボタンをクリックしてフォームを開きます

3) 「GO」ボタンを 12 回または 2 回クリックしてスパムし、メモリ使用量を監視します。これで、リークに気付くはずです。

4a) フォームを閉じます -メモリは解放されません。

また

4b)「CLEAN」ボタンを数回スパムすると、メモリが解放されます。これは、これが参照リークではなく、GC/ファイナライズの問題であることを示しています

私がする必要があるのは、ステップ 3) でリークを防止し、ステップ 4a) でメモリを解放することです。「CLEAN」ボタンは実際のアプリケーションにはありません。隠し参照がないことを示すためだけにここに表示されています。

「GO」ボタンを数回押した後、CLR プロファイラーを使用してメモリ プロファイルを確認しました (この時点でのメモリ使用量は ~350 MB でした)。16125 (ドキュメント内の量の 5 倍)Controls.TextBoxと 16125があり、どちらもファイナライズ キューにルートされているControls.TextBoxView16125オブジェクトにルートされていることがわかりました。こちらを参照してください。Documents.TextEditor

http://i.imgur.com/m28Aiux.png何とか

任意の洞察をいただければ幸いです。

別の更新 - 解決済み (一種)

ElementHostまたは を使用しない別の純粋な WPF アプリケーションでこれに再び遭遇したFlowDocumentので、振り返ってみると、タイトルは誤解を招くものです。Anton Tykhyy が説明したように、これは単に WPFTextBox自体のバグであり、そのTextEditor.

Anton が提案した回避策は好きではありませんでしたが、彼のバグの説明は、やや醜いが短い解決策に役立ちました。

を含むコントロールのインスタンスを破棄しようとしているときは、TextBoxes(コントロールのコード ビハインドで) これを行います。

        var textBoxes = FindVisualChildren<TextBox>(this).ToList();
        foreach (var textBox in textBoxes)
        {
            var type = textBox.GetType();
            object textEditor = textBox.GetType().GetProperty("TextEditor", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(textBox, null);
            var onDetach = textEditor.GetType().GetMethod("OnDetach", BindingFlags.NonPublic | BindingFlags.Instance);
            onDetach.Invoke(textEditor, null);
        }

どこFindVisualChildrenにある:

    public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                if (child != null && child is T)
                {
                    yield return (T)child;
                }

                foreach (T childOfChild in FindVisualChildren<T>(child))
                {
                    yield return childOfChild;
                }
            }
        }
    }

基本的に、私はTextBoxすべきことをします。最後に、私も呼び出しますGC.Collect()(厳密には必要ではありませんが、メモリをより速く解放するのに役立ちます)。これは非常に醜い解決策ですが、問題を解決しているようです。TextEditorsファイナライズ キューでスタックすることはもうありません。

4

2 に答える 2

2

ここでこのブログ投稿を見つけました: Windows フォーム プロジェクト内で WPF ユーザー コントロールを使用しているときに ElementHost を使用しているときにメモリ リークが発生する

したがって、Button2 クリック イベントでこれを試してください。

if (elementHost1 != null)
{
    elementHost1.Child = null;
    elementHost1.Dispose();
    elementHost1.Parent = null;
    elementHost1 = null;
}

この後に GC.Collect() を呼び出しても、メモリ使用量がすぐには減らない可能性がありますが、特定のポイント以降は増加しません。再現性を高めるために、2 番目のフォームを作成しましたForm1。これで、フォームを約20回開いてみました。常にButton1をクリックしてからButton2をクリックしてからフォームを閉じましたが、メモリ使用量は一定のままでした。

編集:奇妙なことに、GC.Collect()ではなく、フォームを再度開いた後にメモリが解放されるようです。ElementHostこれはコントロールのバグだと思わざるを得ません。

Edit2、私のForm1

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        m_uc1 = new UserControl1();
        elementHost1.Child = m_uc1;
    }

    private UserControl1 m_uc1;

    private void button1_Click(object sender, EventArgs e)
    {
        string rawXamlText = File.ReadAllText(@"in.txt");
        var flowDocumentStringReader = new StringReader(rawXamlText);            
        var flowDocumentTextReader = new XmlTextReader(flowDocumentStringReader);           
        object document = XamlReader.Load(flowDocumentTextReader);
        var fd = document as FlowDocument;

        m_uc1.docReader.Document = fd;

        flowDocumentTextReader.Close();
        flowDocumentStringReader.Close();
        flowDocumentStringReader.Dispose();

    }        

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        if (elementHost1 != null)
        {
            elementHost1.Child = null;
            elementHost1.Dispose();
            elementHost1.Parent = null;
            elementHost1 = null;
        }
    }

明示的な GC.Collect() がなくても、メモリ リークはもう発生しません。覚えておいてください、私はこのフォームを別のフォームから複数回開いてみました。

于 2013-02-07T18:55:45.273 に答える
1

確かにPresentationFramework.dll!System.Windows.Documents.TextEditor、ファイナライザーがあるため、適切に破棄しない限り、ファイナライザー キューに (ぶら下がっているすべてのものと一緒に) スタックします。少し調べてみましたが、残念ながら、 es に添付されたs を破棄PresentationFramework.dllさせる方法がわかりません。関連する呼び出しはだけです。ここで、 a が を作成すると、新しいものを作成する代わりにそれを破棄するだけであることがわかります。アプリケーション ドメインがアンロードされるとき、または WPF ディスパッチャがシャットダウンされるときです。シャットダウンされた WPF ディスパッチャを再起動する方法が見つからなかったため、前者の方が少し有望に見えます。WPF オブジェクトはアプリケーション ドメイン間で直接共有できません。TextBoxTextEditorTextBox.OnDetachTextBoxBase.InitializeTextContainer()TextBoxTextEditorTextEditorMarshalByRefObject、しかし Windows フォーム コントロールはそうします。ElementHost別のアプリケーション ドメインに配置して、フォームをクリアするときに破棄してみてください(最初にディスパッチャをシャットダウンする必要がある場合があります)。もう 1 つの方法は、MAF アドインを使用して、WPF コントロールを別のアプリケーション ドメインに配置することです。このSOの質問を参照してください。

于 2013-02-18T14:46:54.917 に答える