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

标签 c# wpf winforms flowdocument elementhost

[更新,见底部!]

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

应用程序的作用

当我按下 button1 :

  • UserControl1其中只包含一个 FlowDocumentReader创建并设置为 ElementHostChild
  • FlowDocument从文本文件创建(它只包含一个 FlowDocument 和一个 StackPanel 和几千行 <TextBox/> )
  • FlowDocumentReaderDocument属性设置为此 FlowDocument

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

    问题
  • button1再次单击,内存使用量增加,并且每次重复该过程时都会增加!尽管使用了大量新内存,但 GC 没有收集!没有不应该存在的引用,因为:
  • 如果我按 button2其中集 elementHost1.Child为 null 并调用 GC(参见下面的代码),另一个奇怪的事情发生了 - 它不会清理内存,但如果我继续点击它几秒钟,它最终会释放它!

  • 对我们来说,所有这些内存都被使用是 Not Acceptable 。此外,删除 ElementHost来自 Controls收藏, Disposing它,将引用设置为空,然后调用 GC 不会释放内存。

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

  • 这不是内存使用无关紧要的事情,我可以让 GC 随时收集它。它实际上最终会显着降低机器的速度。

    编码

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

    否则,这是相关的代码。只需在设计器中的表单中添加 2 个按钮并将它们与事件 Hook 。 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();
            }
        }
    }
    

    用户控件1.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 ,每次按下按钮时都会处理并重新创建它。虽然这确实有点帮助,但从某种意义上说,当您发送垃圾邮件单击 button1 时内存会上下波动,而不是仅仅上升,但它仍然没有解决问题 - 内存总体上升,并且在以下情况下不会被释放表格已关闭。所以现在我悬赏了。

    由于似乎对这里出了什么问题有些困惑,以下是重现泄漏的确切步骤:

    1)打开任务管理器

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

    3)在“GO”按钮上点击一打或两次垃圾邮件并观察内存使用情况 - 现在你应该注意到泄漏

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

    或者

    4b) 多次向“CLEAN”按钮发送垃圾邮件,内存将被释放 ,说明这不是引用泄漏,是GC/finalization问题

    我需要做的是在步骤 3) 中防止泄漏并在步骤 4a) 中释放内存。 “CLEAN”按钮在实际应用中并不存在,它只是为了表明没有隐藏的引用。

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

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

    任何见解表示赞赏。

    另一个更新 - 已解决(有点)

    我刚刚在另一个不使用 ElementHost 的纯 WPF 应用程序中再次遇到了这个问题。或 FlowDocument ,所以回想起来,标题是误导性的。正如 Anton Tykhyy 所解释的,这只是 WPF TextBox 的一个错误。本身,它没有正确处理它的 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卡在完成队列中。

    最佳答案

    我在这里找到了这篇博文: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() 我也不会再遇到任何内存泄漏。请记住,我尝试从另一个表单多次打开此表单。

    关于c# - ElementHost + FlowDocument = GC 不工作,内存不断增加,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14757527/

    相关文章:

    vb.net - KeyDown 事件未通过 .NET WinForms 触发?

    c# - 如何使用 C# 在 JavaScript 中创建的自定义对象上调用方法?

    c# - 计时器不包含在 Xamarin.Forms 的 System.Threading 中

    c# - 如何合并 Lambda 委托(delegate)

    c# - 如何让日期选择器默认为 "Pick a Date"?

    IronPython 的 WPF/Winforms 编辑器控件,具有代码完成功能

    c# - 我的应用程序中的 mySQL 查询出了什么问题?

    wpf - 如何为 WPF 中的单选按钮绑定(bind) Checked 事件?

    c# - C# 中的文本框事件

    c# - 在应用程序关闭时关闭另一个进程