【问题标题】:ElementHost + FlowDocument = GC not working, memory keeps increasingElementHost + FlowDocument = GC 不工作,内存不断增加
【发布时间】:2013-02-07 17:44:15
【问题描述】:

[已更新,见底部!]

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

应用程序做什么

当我按下button1:

  • 创建了一个只包含FlowDocumentReaderUserControl1,并将其设置为ElementHostChild
  • FlowDocument 是从一个文本文件创建的(它只包含一个 FlowDocument 和一个 StackPanel 以及几千行 <TextBox/>
  • FlowDocumentReaderDocument 属性设置为此FlowDocument

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

问题

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

  • 1234563几秒钟,它最终会释放它!

我们无法接受所有这些内存都被使用。此外,从Controls 集合中删除ElementHostDisposing 它,将引用设置为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();
        }
    }
}

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”按钮十几或两次并观察内存使用情况 - 现在您应该注意到泄漏

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

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

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

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

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

任何见解都值得赞赏。

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

我刚刚在另一个不使用ElementHostFlowDocument 的纯WPF 应用程序中再次遇到这个问题,所以回想起来,这个标题具有误导性。正如 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 卡在终结队列中了。

【问题讨论】:

  • StringReaderXmlTextReader 都有 Close() 方法,我想说调用它们不会有什么坏处,即使在 using 块中也是如此。除此之外,我可以重现您的问题,您是否尝试过使用像 Ants 这样的内存分析器?我发现这很有帮助,但现在还没有安装在机器上。
  • 您在哪里处理分配给变量 uc1 的 UserControl?将子级设置为您的 elementhost 的 null 是不够的
  • @Jobo 我忘记了这样做,并且目前不在计算机附近,但是我确实在我们的实际应用程序中使用了 .net 分析器。它显示了一堆 WPF 对象,最终都植根于终结器队列中的 System.Documents.TextEditor 对象(或更多,不确定)。我很确定“使用”对读者来说很好。
  • @Jehof,uc1 是一个局部变量,所以它超出了范围,当我将剩余的引用 elementHost1.Child 设置为 null 时,它是一个可以被垃圾收集的死对象。 UserControl 不实现 IDisposable。事实上,正如我在问题中所写,如果我一直按下 button2,内存就会被垃圾收集。
  • 好的,我明白了。它是一个 wpf 用户控件。我认为这是 Form 类的 winforms 原因

标签: c# wpf winforms flowdocument elementhost


【解决方案1】:

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

【讨论】:

  • 该博客文章中的泄漏来自 ElementHost 在父对象的 Controls 集合中保持引用,而他一直在分配新的 ElementHost。另一方面,我只有 1 个 ElementHost,并且我一直在重复使用它。正如我所提到的,将其移除并丢弃并没有帮助。
  • 无论如何,它似乎解决了这个问题,至少对我来说是这样。你试过了吗?
  • 它如何为您解决问题?如果连续点击button1 20次,内存不涨?我不能将 GC 的东西放在任何地方或放在计时器上,因为实际应用程序很大,所以速度非常慢。我有能力把它放在可以运行 1 次并且需要释放所有内存的地方。理想情况下,我根本不会使用它。是的,我提到这个解决方案在问题中不起作用。
  • 查看我的编辑。内存当然会增加一点,但会保持低位且恒定。
  • 是的,如果我继续关闭并重新打开表单,内存最终会下降。这不是因为 Form1_FormClosing 中的代码,如果您删除该代码,它仍然会发生,因为表单在处置时处置它的子控件。但这并不能解决我的问题,请阅读“我想要什么”部分。
【解决方案2】:

确实,PresentationFramework.dll!System.Windows.Documents.TextEditor 有一个终结器,因此除非处理得当,否则它会卡在终结器队列中(连同挂在它上面的所有东西)。我在PresentationFramework.dll 中搜索了一下,不幸的是我不知道如何让TextBoxes 处理他们附加的TextEditors。对TextBox.OnDetach 的唯一相关调用是在TextBoxBase.InitializeTextContainer() 中。在那里您可以看到,一旦TextBox 创建了TextEditor,它只会丢弃它以换取创建一个新的。 TextEditor 自行处理的另外两个条件是应用程序域卸载或 WPF 调度程序关闭时。前者看起来更有希望,因为我发现无法重新启动关闭的 WPF 调度程序。 WPF 对象不能直接在应用程序域之间共享,因为它们不是从MarshalByRefObject 派生的,但 Windows 窗体控件可以。尝试将您的 ElementHost 放在一个单独的应用程序域中,并在清除表单时将其删除 (you may need to shut down the dispatcher first)。另一种方法是使用 MAF 插件将您的 WPF 控件放入不同的应用程序域;见this SO question

【讨论】:

  • 我认为你是对的,但我不确定如何解决它。如何在不同的 AppDomain 中托管 ElementHost?我还需要数据绑定...我尝试将整个表单托管在另一个 AppDomain 中,并关闭调度程序+卸载域+强制垃圾收集确实有效,但这在实际应用程序中不可行。我不明白的是,如果在卸载 AppDomain 后 Collect/WaitForPendingFinalizers/Collect 工作,为什么没有它就不能工作并且对象仍保留在 Finalizer 队列中?
  • 将 WPF 相关的东西包装到 MarshalByRefObject 派生类中,在那里创建 ElementHost 和 WPF 填充物。然后您可以 CreateInstanceAndUnwrap 这个类并获取 ElementHost,CLR 将为您编组它。如果您有重要的数据绑定,事情就会变得复杂,并且很大程度上取决于确切的细节。卸载 appdomain 后,您不需要强制 GC,因为卸载它会从终结器队列中删除 TextEditor 对象。
  • 我放弃了。我不喜欢 AppDomain 解决方案,我找不到终结器没有运行的原因......哦,好吧。
  • 您的意思是在GC.WaitForPendingFinalizers() 之前和之后的终结器队列中有相同数量的TextEditors?你检查过WinDbg吗?如果是这样,很可能是 CLR 错误,如果您可以在独立的演示项目中重现它,请向 MS Connect 报告。
  • 是的,它不会改变 - 有时。有时会。因此,如果我继续单击调用 collect/waitfor../collect 的按钮,它最终会起作用。但是 TextEditor 对象一直都在完成队列中 - 至少我认为它们是 - 它看起来像我发布的屏幕截图中一样。我没有用WinDbg检查过它,因为我从来没有使用过它。
猜你喜欢
  • 1970-01-01
  • 2014-11-13
  • 2014-06-14
  • 2023-03-09
  • 2021-03-10
  • 2017-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多