[更新、下部を参照してください!]
FlowDocumentReader
で WPFをホストしている WinForms アプリケーションにメモリ リークがありますElementHost
。この問題を単純なプロジェクトで再現し、以下のコードを追加しました。
アプリケーションの機能
押すとbutton1
:
UserControl1
のみを含むがFlowDocumentReader
作成され、 に設定されElementHost
ますChild
- は
FlowDocument
テキスト ファイルから作成されます (数千行の とFlowDocument
が含まれているだけです) 。StackPanel
<TextBox/>
- のプロパティはこれに設定されてい
FlowDocumentReader
ますDocument
FlowDocument
この時点で、ページはFlowDocument
正しくレンダリングされます。予想通り、大量のメモリが使用されます。
問題
をもう一度クリックする
button1
と、メモリ使用量が増加し、プロセスが繰り返されるたびに増加し続けます! 大量の新しいメモリが使用されているにもかかわらず、GC が収集されません! 次の理由により、そこにあってはならない参照はありません。whichを null
button2
に設定elementHost1.Child
して GC を呼び出すと (以下のコードを参照)、別の奇妙なことが起こります。メモリはクリーンアップされませんが、数秒間クリックし続けると、最終的にメモリが解放されます!
このすべてのメモリが使用されたままになることは、私たちにとって容認できません。また、コレクションElementHost
からを削除し、参照を null に設定してから GC を呼び出しても、メモリは解放されません。Controls
Disposing
私が欲しいもの
- が複数回クリックされた場合
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.TextBoxView
16125オブジェクトにルートされていることがわかりました。こちらを参照してください。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
ファイナライズ キューでスタックすることはもうありません。