问题 通过附加行为防止内存泄漏


我在我的WPF应用程序中创建了一个“附加行为”,它允许我处理Enter按键并移动到下一个控件。我将其称为EnterKeyTraversal.IsEnabled,您可以在我的博客上看到代码 这里

我现在主要担心的是我可能有内存泄漏,因为我在UIElements上处理PreviewKeyDown事件并且从未明确地“解开”事件。

什么是防止这种泄漏的最佳方法(如果有的话)?我应该保留我正在管理的元素列表,并在Application.Exit事件中取消挂起PreviewKeyDown事件吗?有没有人在他们自己的WPF应用程序中成功附加行为,并提出了一个优雅的内存管理解决方案?


11623
2017-08-18 00:49


起源



答案:


我不同意DannySmurf

一些WPF布局对象可能会阻塞您的内存,并且当您的应用程序没有被垃圾回收时,它们会非常慢。因此,我发现选择的单词是正确的,您正在将内存泄露给不再使用的对象。您希望这些项目是垃圾收集的,但它们不是,因为某处有一个引用(在本例中是来自事件处理程序)。

现在回答真实的答案:)

我建议你读这个 MSDN上的WPF性能文章

不删除对象上的事件处理程序   可以保持对象活着

对象传递给的委托   它的事件实际上是一个参考   到那个对象。因此,事件   处理程序可以使对象保持更长时间   比预期。进行清洁时   注册的对象的一部分   听一个对象的事件,它是   删除该委托至关重要   在释放对象之前。保持   不需要的物体活着增加了   应用程序的内存使用情况。这是   当对象是。时尤其如此   逻辑树或视觉的根   树。

他们建议你调查一下 弱事件模式

另一种解决方案是在完成对象后删除事件处理程序。但我知道,对于附加属性,这一点可能并不总是很清楚。

希望这可以帮助!


4
2017-08-18 08:39



我正在投票,因为答案与实际问题无关。每个人都知道内存泄漏的坑及其原因,特别是由于你提到的事件处理程序。 @Matt在这里想知道的是如何在使用内部附加行为时安全地处理事件处理程序。我很快就会回答这个问题。 - Saraf Talukder


答案:


我不同意DannySmurf

一些WPF布局对象可能会阻塞您的内存,并且当您的应用程序没有被垃圾回收时,它们会非常慢。因此,我发现选择的单词是正确的,您正在将内存泄露给不再使用的对象。您希望这些项目是垃圾收集的,但它们不是,因为某处有一个引用(在本例中是来自事件处理程序)。

现在回答真实的答案:)

我建议你读这个 MSDN上的WPF性能文章

不删除对象上的事件处理程序   可以保持对象活着

对象传递给的委托   它的事件实际上是一个参考   到那个对象。因此,事件   处理程序可以使对象保持更长时间   比预期。进行清洁时   注册的对象的一部分   听一个对象的事件,它是   删除该委托至关重要   在释放对象之前。保持   不需要的物体活着增加了   应用程序的内存使用情况。这是   当对象是。时尤其如此   逻辑树或视觉的根   树。

他们建议你调查一下 弱事件模式

另一种解决方案是在完成对象后删除事件处理程序。但我知道,对于附加属性,这一点可能并不总是很清楚。

希望这可以帮助!


4
2017-08-18 08:39



我正在投票,因为答案与实际问题无关。每个人都知道内存泄漏的坑及其原因,特别是由于你提到的事件处理程序。 @Matt在这里想知道的是如何在使用内部附加行为时安全地处理事件处理程序。我很快就会回答这个问题。 - Saraf Talukder


除了哲学辩论之外,在查看OP的博客文章时,我没有看到任何泄漏:

ue.PreviewKeyDown += ue_PreviewKeyDown;

很难参考 ue_PreviewKeyDown 存储在 ue.PreviewKeyDown

ue_PreviewKeyDown 是一个 STATIC 方法并不能 GCed

没有硬性参考 ue 正在存储,所以没有任何东西阻止它存在 GCed

那么......泄漏在哪里?


4
2018-05-29 18:56



这是一个常见的误解。 ue.PreviewKeyDown + = ue_PreviewKeyDown保持对ue的强引用,因为us_PreviewKeyDown是静态的,并且永远不会收集ue。 - SACO
@SACO你可以解释一下吗?在哪里保留“对ue的强烈提及”?据我所见,John完全正确,原始示例中绝对没有内存泄漏。 ue.PreviewKeyDown -= ue_PreviewKeyDown 没有必要。 - Golvellius
@Golvellius我发布了一个答案,可以解释我的观点。我实际上现在测试了它,发现如果ue_PreviewKeyDown是静态的,那么就不会有泄漏。 - SACO


是的,我知道在过去,Memory Leaks是一个完全不同的主题。但是对于托管代码,内存泄漏一词的新含义可能更合适......

微软甚至承认它是一个内存泄漏:

为什么要实现WeakEvent模式?

倾听事件可能导致   内存泄漏。典型的技术   听取事件就是使用   语言特定的语法   将处理程序附加到a上的事件   资源。例如,在C#中,那   语法是:source.SomeEvent + = new   SomeEventHandler(一个MyEventHandler)。

这种技术创造了强大   从事件源引用到   事件监听器。通常,附加   监听器的事件处理程序导致   听众有一个对象   受物体影响的一生   来源的生命周期(除非   事件处理程序被明确删除)。   但在某些情况下你可能会   想要对象的生命周期   听众只能被控制   其他因素,如是否   目前属于可视化树   应用程序,而不是   源头的生命周期。每当   源对象的生命周期超出了   监听器的对象生存期,   正常事件模式导致a   内存泄漏:保留了监听器   活得比预期长。

我们将WPF用于具有大型ToolWindows的客户端应用程序,可以拖放,所有漂亮的东西,以及所有兼容的XBAP ..但我们遇到了一些没有垃圾收集的ToolWindows同样的问题..这是应该的事实上它仍然依赖于事件监听器..现在,当您关闭窗口并关闭应用程序时,这可能不是问题。但是如果你用很多命令创建非常大的ToolWindows,并且一遍又一遍地重新评估所有这些命令,人们必须整天使用你的应用程序..我可以告诉你..它真的堵塞了你的记忆和你的应用程序的响应时间..

此外,我发现更容易向我的经理解释我们有内存泄漏,而不是向他解释由于某些需要清理的事件而导致某些对象没有被垃圾收集;)


4
2017-08-18 14:44





@Nick是的,附加行为的事情是,根据定义,它们与您正在处理的事件的元素不在同一个对象中。

我认为答案在于以某种方式使用WeakReference,但我没有看到任何简单的代码示例来向我解释。 :)


2
2017-08-18 01:51





您是否实施了“弱事件模式”而不是常规事件?

  1. WPF中的弱事件模式
  2. 弱事件模式(MSDN)

1
2017-08-18 08:48





为了解释我对约翰芬顿的评论,这里是我的答案。让我们看看以下示例:

class Program
{
    static void Main(string[] args)
    {
        var a = new A();
        var b = new B();

        a.Clicked += b.HandleClicked;
        //a.Clicked += B.StaticHandleClicked;
        //A.StaticClicked += b.HandleClicked;

        var weakA = new WeakReference(a);
        var weakB = new WeakReference(b);

        a = null;
        //b = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Console.WriteLine("a is alive: " + weakA.IsAlive);
        Console.WriteLine("b is alive: " + weakB.IsAlive);
        Console.ReadKey();
    }


}

class A
{
    public event EventHandler Clicked;
    public static event EventHandler StaticClicked;
}

class B
{
    public void HandleClicked(object sender, EventArgs e)
    {
    }

    public static void StaticHandleClicked(object sender, EventArgs e)
    {
    }
}

如果你有

a.Clicked += b.HandleClicked;

并且只将b设置为null两个引用weakA和weakB保持活着!如果你只设置一个空值b保持活着而不是一个(这证明John Fenton错误地说明硬件引用存储在事件提供者中 - 在这种情况下是a)。

这导致我得出错误的结论

a.Clicked += B.StaticHandleClicked;

会导致泄漏,因为我虽然a的实例将由静态处理程序保存。事实并非如此(测试我的程序)。在静态事件处理程序或事件的情况下,它是相反的方式。如果你写

A.StaticClicked += b.HandleClicked;

参考将保留给b。


1
2018-01-30 15:33



感谢您投入时间回复我的评论,但是:John Fenton没有说明硬件参考存储在事件提供程序中。通过设置 a 为null,你基本上删除了“硬”引用因为内存对象 a 指向之后的垃圾收集。和 a.Clicked += B.StaticHandleClicked; 正是OP的情况 - 这永远不会导致内存泄漏,因此John Fenton的问题 那么......泄漏在哪里?。正如你所指出的那样,只有相反的方向,但这不是OP的情况。 - Golvellius


确保事件引用元素与它们引用的对象一起使用,就像表单控件中的文本框一样。或者,如果无法阻止。在全局帮助器类上创建静态事件,然后监视事件的全局帮助程序类。如果使用WeakReference无法完成这两个步骤,它们通常适用于这些情况,但它们会带来开销。


0
2017-08-18 00:58