防止具有附加行为的内存泄漏

.net wpf memory

5675 观看

11回复

171347 作者的声誉

我在WPF应用程序中创建了一个“附加行为”,使我可以处理Enter键并移至下一个控件。我将其称为EnterKeyTraversal.IsEnabled,您可以在此处的博客上查看代码。

现在,我主要关心的是我可能会发生内存泄漏,因为我正在处理UIElements上的PreviewKeyDown事件,而从不显式“摘机”该事件。

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

作者: Matt Hamilton 的来源 发布者: 2008 年 8 月 18 日

回应 11


0

46933 作者的声誉

确保事件引用元素与它们所引用的对象一起使用,例如表单控件中的文本框。或者,如果无法避免。在全局帮助程序类上创建一个静态事件,然后监视全局帮助程序类中的事件。如果无法完成这两个步骤,请尝试使用WeakReference,它们通常是这些情况的理想选择,但它们会带来开销。

作者: Nick Berardi 发布者: 2008 年 8 月 18 日

2

171347 作者的声誉

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

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

作者: Matt Hamilton 发布者: 2008 年 8 月 18 日

0

14044 作者的声誉

我刚刚读了您的博客文章,我想您会得到一些误导性建议,Matt。如果此处存在实际的内存泄漏,则说明这是.NET Framework中的错误,而不一定是您可以在代码中修复的错误。

我认为您(以及博客的发布者)实际上在这里谈论的并不是泄漏,而是内存的持续消耗。那不是同一回事。为了清楚起见,泄漏的内存是由程序保留,然后被放弃(即,指针悬空)并随后无法释放的内存。由于内存是在.NET中管理的,因此从理论上讲这是不可能的。但是,程序可以保留不断增加的内存,而又不允许对它的引用超出范围(并有资格进行垃圾回收)。但是该内存没有泄漏。程序退出后,GC会将其返回给系统。

所以。要回答您的问题,我认为您实际上没有问题。您当然没有内存泄漏,并且从您的代码来看,就内存消耗而言,我也不用担心。只要确保不取消分配事件处理程序就不会重复分配该事件处理程序(即,您只能设置一次,或者每次分配时都将其删除一次),您似乎在做,您的代码应该没问题。

您的博客上的海报似乎正试图给您这样的建议,但他使用了令人震惊的工作“泄漏”,这是一个令人恐惧的词,但许多程序员却忘记了托管世界的真正含义。它不适用于这里。

作者: TheSmurf 发布者: 2008 年 8 月 18 日

5

23291 作者的声誉

决定

我不同意DannySmurf

一些WPF布局对象可能会阻塞您的内存,并在不进行垃圾回收时使您的应用程序真正变慢。因此,我发现单词的选择是正确的,您正在将内存泄漏给不再使用的对象。您希望这些项目被垃圾回收,但不是,因为在某个地方有一个引用(在这种情况下,来自事件处理程序)。

现在是一个真正的答案:)

我建议您阅读MSDN上的WPF性能文章

不删除对象上的事件处理程序可能会使对象保持活动状态

对象传递给其事件的委托实际上是对该对象的引用。因此,事件处理程序可以使对象的生存期比预期的更长。对已注册以侦听对象事件的对象执行清理时,必须在释放对象之前删除该委托。使不需要的对象保持活动状态会增加应用程序的内存使用量。当对象是逻辑树或可视树的根时,尤其如此。

他们建议您研究弱事件模式

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

希望这可以帮助!

作者: Arcturus 发布者: 2008 年 8 月 18 日

1

11079 作者的声誉

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

  1. WPF中的弱事件模式
  2. 弱事件模式(MSDN)
作者: Patrik Svensson 发布者: 2008 年 8 月 18 日

0

14044 作者的声誉

@Arcturus:

...阻塞您的内存,并在不进行垃圾收集时使您的应用程序真正变慢。

这是显而易见的,我不同意。然而:

...您正在将内存泄漏给不再使用的对象...,因为有对它们的引用。

“将内存分配给程序,并且由于程序逻辑缺陷,该程序随后失去了访问它的能力”(维基百科,“内存泄漏”)

如果有一个对象的活动引用,程序可以访问该对象,则根据定义,它不会泄漏内存。泄漏意味着对象(您或OS /框架)将不再可访问,并且在操作系统当前会话的整个生命周期内都不会释放该对象。这里不是这种情况。

(对不起,这是一个语义纳粹……也许我有点老派了,但是泄漏具有非常特殊的含义。如今,人们倾向于使用“内存泄漏”来表示任何东西比他们想要的多消耗2KB的内存。 ..)

但是,当然,如果您不释放事件处理程序,则在关闭过程中,垃圾收集器将您的进程的内存回收之前,不会释放附加到该事件处理程序的对象。但是,这种行为完全是预期的,与您似乎暗示的相反。如果您希望回收一个对象,则需要删除所有可能使引用保持活动状态的内容,包括事件处理程序。

作者: TheSmurf 发布者: 2008 年 8 月 18 日

4

23291 作者的声誉

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

微软甚至承认这是内存泄漏:

为什么要实现WeakEvent模式?

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

此技术创建了从事件源到事件侦听器的强大参考。通常,为侦听器附加事件处理程序会使侦听器具有受源对象生存期影响的对象生存期(除非显式删除了事件处理程序)。但是在某些情况下,您可能希望仅通过其他因素来控制侦听器的对象生存期,例如它当前是否属于应用程序的可视树,而不是由源生存期来控制。只要源对象的生存期超过侦听器的对象生存期,正常事件模式就会导致内存泄漏:侦听器的生存期比预期的更长。

我们将WPF用于具有大型ToolWindows的客户端应用程序,可以将其拖放,所有漂亮的东西,并且都与XBAP兼容。.但是,对于某些未垃圾收集的ToolWindows,我们也遇到了同样的问题。因为它仍然依赖于事件侦听器。.现在,当您关闭窗口并关闭应用程序时,这可能不是问题。但是,如果您要使用许多命令创建非常大的ToolWindows,并且一遍又一遍地重新评估所有这些命令,那么人们必须整天使用您的应用程序。我可以告诉您..这确实阻塞了您的内存和您应用的响应时间。

另外,我发现向我的经理解释我们有内存泄漏要比向他解释一些对象由于某些需要清理的事件而没有被垃圾回收要容易得多;)

作者: Arcturus 发布者: 2008 年 8 月 18 日

-1

14044 作者的声誉

我当然可以理解并表示同情(经理位)。

但是无论微软怎么称呼,我都不认为“新”定义是合适的。这很复杂,因为我们不生活在一个100%受管的世界中(即使Microsoft喜欢假装我们这样做,Microsoft本身也不生活在这样一个世界中)。当您说内存泄漏时,您可能意味着程序正在消耗过多的内存(这是用户的定义),或者直到退出时才释放托管引用(如此处所示),或者未正确清理非托管引用。上升(这可能是真正的内存泄漏),或者从托管代码调用的非托管代码正在泄漏内存(另一个实际泄漏)。

在这种情况下,即使我们不精确,“内存泄漏”的含义也很明显。但是,与某些人的交谈变得非常乏味,这些人称每次消耗过多或未能收集到内存泄漏为生。当这些人是程序员,他们应该更了解时,这真令人沮丧。我认为技术术语具有明确的意义是很重要的。当他们这样做时,调试非常容易。

无论如何。并不是要把它变成关于语言的童话讨论。只是说...

作者: TheSmurf 发布者: 2008 年 8 月 18 日

0

23291 作者的声誉

对,对,

您当然是对的。但是,这个世界诞生了新一代的程序员,他们永远都不会接触非托管代码,而且我确实相信语言定义会一遍又一遍地重新发明自己。WPF中的内存泄漏以这种方式不同于C / Cpp。

当然,对我的经理们而言,我将其称为内存泄漏..对我的同事们而言,我将其称为性能问题!

关于Matt的问题,可能是您需要解决的性能问题。如果仅使用几个屏幕并使这些屏幕控件成为单例,则可能根本看不到此问题;)。

作者: Arcturus 发布者: 2008 年 8 月 18 日

5

51 作者的声誉

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

ue.PreviewKeyDown += ue_PreviewKeyDown;

的硬引用ue_PreviewKeyDown存储在中ue.PreviewKeyDown

ue_PreviewKeyDown是一种STATIC方法,不能GCed

没有硬引用ue被存储,因此没有什么阻止它GCed

所以...泄漏在哪里?

作者: John Fenton 发布者: 2011 年 5 月 29 日

2

1282 作者的声誉

在这里解释我对John Fenton帖子的评论是我的答案。让我们看下面的例子:

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均保持有效!如果仅将a设置为null,则b保持有效,而a则不存在(这证明John Fenton指出在事件提供程序中存储了硬引用是错误的-在这种情况下为a)。

这导致我得出错误的结论,即

a.Clicked += B.StaticHandleClicked;

会导致泄漏,因为我虽然a的实例将由静态处理程序保留。事实并非如此(请测试我的程序)。对于静态事件处理程序或事件,则相反。如果你写

A.StaticClicked += b.HandleClicked;

将保留对b的引用。

作者: Daniel Bişar 发布者: 2015 年 1 月 30 日
32x32