5

[更新,见底部!]

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

应用程序的作用

当我按下button1

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

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

问题

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

  • 如果我按下button2which setelementHost1.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();
        }
    }
}

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/m28Auix.png废话

任何见解表示赞赏。

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

我刚刚在另一个不使用 anElementHost或 a 的纯 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停留在完成队列中。

4

2 回答 2

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控件的错误。

编辑2,我的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() 我也不会再遇到任何内存泄漏。请记住,我尝试从另一个表单多次打开此表单。

于 2013-02-07T18:55:45.273 回答
1

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

于 2013-02-18T14:46:54.917 回答