加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-15 05:37:53 所属栏目:百科 来源:网络整理
导读:[更新,见底!] 我们的WinForms应用程序中存在内存泄漏,在ElementHost中托管WPF FlowDocumentReader.我在一个简单的项目中重新创建了这个问题,并添加了下面的代码. 该应用程序的功能 当我按下按钮1时: 创建一个仅包含FlowDocumentReader的UserControl1并将其
[更新,见底!]

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

该应用程序的功能

当我按下按钮1时:

>创建一个仅包含FlowDocumentReader的UserControl1并将其设置为ElementHost的Child
>从文本文件创建FlowDocument(它只包含一个带有StackPanel的FlowDocument,其中包含几千行< TextBox />)
> FlowDocumentReader的Document属性设置为此FlowDocument

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

问题

>如果再次单击button1,则内存使用量会增加,并且每次重复该过程时都会继续增加!尽管使用了大量新内存,GC仍未收集!没有参考不应该存在,因为:
>如果我按下button2将elementHost1.Child设置为null并调用GC(参见下面的代码),会发生另一个奇怪的事情 – 它不会清理内存,但如果我一直点击它几秒钟,它最终会免费吧!

我们所有这些记忆都被使用是不可接受的.此外,从Controls集合中删除ElementHost,Disposing it,将引用设置为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,而是在每次按下按钮时处理并重新创建它.虽然这确实有点帮助,但是当你垃圾邮件点击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

有任何见解赞赏.

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

我刚刚在另一个不使用ElementHost或FlowDocument的纯WPF应用程序中遇到了这个问题,所以回想起来,标题是误导性的.正如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);
        }

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卡在终结队列中.

解决方法

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

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读