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

svi*_*nja 5 c# wpf elementhost flowdocument winforms

[更新,见底!]

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

该应用程序的功能

当我按下button1:

  • UserControl1只包含a的A FlowDocumentReader被创建并设置为ElementHost'sChild
  • A FlowDocument是从文本文件创建的(它只包含一个FlowDocument带有StackPanel几千行的文件<TextBox/>)
  • FlowDocumentReaderDocument属性设置为这FlowDocument

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

问题

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

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

我们所有这些记忆都被使用是不可接受的.此外,ElementHostControls集合中删除Disposing它,将引用设置为null,然后调用GC不会释放内存.

我想要的是

  • 如果button1点击多次,内存使用量不应该继续增加
  • 我应该能够释放所有内存(这只是"真实"应用程序中的一个窗口,我希望在它关闭时执行此操作)

这不是内存使用无关紧要的事情,我可以随时让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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

编辑:

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

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

1)打开任务管理器

2)单击"开始"按钮打开表单

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

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

要么

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

我需要做的是在步骤3)防止泄漏并在步骤4a)释放存储器.实际应用程序中没有"CLEAN"按钮,只是在这里显示没有隐藏的引用.

我使用CLR分析器在点击"GO"按钮几次后检查内存配置文件(此时内存使用量约为350 MB).事实证明,有16125(文档中的数量的5倍)Controls.TextBox和16125 Controls.TextBoxView都植根于16125个Documents.TextEditor根目录在终结队列中的对象 - 请参阅此处:

http://i.imgur.com/m28Aiux.png胡说

有任何见解赞赏.

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

我刚刚在另一个不使用a ElementHost或a的纯WPF应用程序中遇到了这个FlowDocument,所以回想起来,标题是误导性的.正如Anton Tykhyy所解释的,这只是WPF 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);
        }
Run Code Online (Sandbox Code Playgroud)

在哪里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;
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

基本上,我做了TextBox应该做的事情.最后我也打电话GC.Collect()(不是绝对必要但有助于更快地释放内存).这是一个非常难看的解决方案,但它似乎解决了这个问题.不再TextEditors卡在终结队列中.

Ant*_*hyy 1

事实上,PresentationFramework.dll!System.Windows.Documents.TextEditor它有一个终结器,因此除非正确处理,否则它会卡在终结器队列中(连同挂在其上的所有内容)。我四处搜寻了一下PresentationFramework.dll,不幸的是我不知道如何让TextBoxes 处理它们附加的TextEditors。唯一相关的调用TextBox.OnDetach是在TextBoxBase.InitializeTextContainer(). 在那里您可以看到,一旦 aTextBox创建了 a TextEditor,它只会处理它以换取创建一个新的。自行处置的另外两个条件TextEditor是应用程序域卸载或 WPF 调度程序关闭。前者看起来更有希望,因为我发现无法重新启动已关闭的 WPF 调度程序。WPF 对象不能直接跨应用程序域共享,因为它们不是派生自 的MarshalByRefObject,但 Windows 窗体控件可以。尝试将您的ElementHost应用程序域放在一个单独的应用程序域中,并在清除表单时将其拆除(您可能需要先关闭调度程序)。另一种方法是使用 MAF 加载项将 WPF 控件放入不同的应用程序域中;看到这个问题