【发布时间】:2013-02-07 17:44:15
【问题描述】:
[已更新,见底部!]
在ElementHost 中托管 WPF FlowDocumentReader 的 WinForms 应用程序存在内存泄漏。我在一个简单的项目中重新创建了这个问题并添加了下面的代码。
应用程序做什么
当我按下button1:
- 创建了一个只包含
FlowDocumentReader的UserControl1,并将其设置为ElementHost的Child -
FlowDocument是从一个文本文件创建的(它只包含一个FlowDocument和一个StackPanel以及几千行<TextBox/>) -
FlowDocumentReader的Document属性设置为此FlowDocument
此时,页面正确呈现FlowDocument。正如预期的那样,使用了大量内存。
问题
如果再次单击
button1,内存使用量会增加,并且每次重复该过程都会不断增加!尽管正在使用大量新内存,但 GC 并未收集!没有不应该存在的引用,因为:
1234563几秒钟,它最终会释放它!
我们无法接受所有这些内存都被使用。此外,从Controls 集合中删除ElementHost,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/m28Aiux.png
任何见解都值得赞赏。
另一个更新 - 已解决(有点)
我刚刚在另一个不使用ElementHost 或FlowDocument 的纯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 卡在终结队列中了。
【问题讨论】:
-
StringReader和XmlTextReader都有 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