ElementHost + FlowDocument = GC不工作,内存不断增加

时间:2013-02-07 17:44:16

标签: c# wpf winforms flowdocument elementhost

[已更新,请参阅底部!]

FlowDocumentReader中托管WPF ElementHost的WinForms应用程序中存在内存泄漏。我在一个简单的项目中重新创建了这个问题,并添加了下面的代码。

应用程序做什么

当我按button1时:

  • 创建了仅包含UserControl1的{​​{1}}并将其设置为FlowDocumentReader的{​​{1}}
  • ElementHost是从文本文件创建的(它只包含一个ChildFlowDocument只有几千行FlowDocument
  • StackPanel的{​​{1}}属性设置为此<TextBox/>

此时,页面正确呈现FlowDocumentReader。正如预期的那样,使用了大量的内存。

问题

  • 如果再次单击Document,内存使用量会增加,并且每次重复该过程时都会不断增加!尽管使用了大量新内存,GC仍未收集!没有引用不应该存在,因为:

  • 如果按FlowDocumentFlowDocument设置为null并调用GC(请参阅下面的代码),会发生另一个奇怪的事情 - 它不会清理内存,但是如果我继续点击它几秒钟,它最终会释放它!

我们不能接受所有这些记忆的使用。此外,从button1集合button2删除elementHost1.Child,将引用设置为null,然后调用GC不会释放内存。

我想要什么

  • 如果多次点击ElementHost,内存使用量不应该继续上升
  • 我应该可以释放所有内存(这只是“真实”应用程序中的一个窗口,我希望在关闭时执行此操作)

这不是内存使用无关紧要的事情,我可以随时让GC收集它。它实际上最终会使机器变慢。

代码

如果您只想下载VS项目,我已在此处上传: http://speedy.sh/8T5P2/WindowsFormsApplication7.zip

否则,这是相关代码。只需在设计器中为表单添加2个按钮,然后将它们连接到事件即可。 Form1.cs中:

Controls

UserControl1.xaml

Disposing

编辑:

我终于有时间再次处理这件事了。我尝试的不是重复使用button1,而是在每次按下按钮时重新设置和重新创建它。虽然这确实有点帮助,但是当你垃圾邮件点击button1而不是仅仅上升时内存正在上下移动时,它仍然无法解决问题 - 内存整体上升并且当它没有被释放时表格已关闭。所以现在我正在给予赏金。

由于这里似乎存在一些混淆,这里有重复泄漏的确切步骤:

1)打开任务管理器

2)点击“开始”按钮打开表格

3)垃圾邮件在“GO”按钮上点击十二次或者点击内存使用情况 - 现在您应该注意到泄漏

4a)关闭表单 - 不会释放内存。

4b)垃圾邮件“CLEAN”按钮几次,内存将被释放,表明这不是引用泄漏,这是GC /终结问题

我需要做的是在步骤3中防止泄漏并在步骤4a)释放内存。实际应用中没有“CLEAN”按钮,它就在这里表明没有隐藏的参考文献。

我使用CLR探查器在点击“GO”按钮几次后检查内存配置文件(此时内存使用量约为350 MB)。事实证明,16125(文档中的数量的5倍)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(); } } } 和16125 <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> 都植根于16125 ElementHost对象,这些对象植根于最终化队列中 - 请参见此处:

http://i.imgur.com/m28Aiux.png blah

有任何见解。

另一个更新 - 解决(种类)

我刚刚在另一个不使用Controls.TextBoxControls.TextBoxView的纯WPF应用程序中再次遇到此问题,所以回想起来,标题会产生误导。正如Anton Tykhyy所解释的,这只是WPF Documents.TextEditor本身的一个错误,它没有正确处理它的ElementHost

我不喜欢安东建议的解决方法,但他对这个错误的解释对我这个相当丑陋但又简短的解决方案很有用。

当我要销毁包含FlowDocument的控件实例时,我这样做(在控件的代码隐藏中):

TextBox

TextEditor的位置:

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; } } } } 卡在终结队列中。

2 个答案:

答案 0 :(得分:2)

我在这里发现了这篇博文:Memory Leak while using ElementHost when using a WPF User control inside a Windows Forms project

因此,请在Button2点击事件中尝试此操作:

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

我发现在此之后调用GC.Collect()它可能不会立即减少内存使用量,但它不会在某一点后面增加。为了更好地再现,我制作了第二张表格,打开了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(),我也不会再遇到任何内存泄漏。请记住,我尝试过多次从另一种形式打开此表单。

答案 1 :(得分:1)

实际上,PresentationFramework.dll!System.Windows.Documents.TextEditor有一个终结器,所以它会被卡在终结器队列上(连同所有挂起它的东西),除非正确处理掉。我在PresentationFramework.dll稍微徘徊,不幸的是我没有看到如何让TextBox es处理他们附加的TextEditor s。对TextBox.OnDetach的唯一相关电话是TextBoxBase.InitializeTextContainer()。在那里,您可以看到TextBox创建TextEditor后,它只会处理它以换取创建新的TextEditorMarshalByRefObject处置自身的另外两个条件是应用程序域卸载或WPF调度程序关闭时。前者看起来更有希望,因为我发现没有办法重新启动关闭的WPF调度程序。 WPF对象不能跨应用程序域直接共享,因为它们不是派生自ElementHost,而是Windows窗体控件。尝试将{{1}}放在单独的应用程序域中,并在清除表单时将其删除(you may need to shut down the dispatcher first)。另一种方法是使用MAF加载项将WPF控件放入不同的应用程序域;见this SO question