使用附加行为防止内存泄漏

12

我在我的WPF应用程序中创建了一个“附加行为”,可以处理Enter键和转移到下一个控件。我称之为EnterKeyTraversal.IsEnabled,并且你可以在我的博客这里看到代码。

我现在的主要担忧是可能存在内存泄漏,因为我正在处理UI元素上的PreviewKeyDown事件,但从未明确地“取消挂钩”该事件。

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

11个回答

5

撇开哲学争论不谈,在查看OP的博客文章时,我没有发现任何泄漏:

ue.PreviewKeyDown += ue_PreviewKeyDown;

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

ue_PreviewKeyDown是一个STATIC方法,无法进行GCed

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

那么...泄漏在哪里?


2
这是一个常见的误解。ue.PreviewKeyDown += ue_PreviewKeyDown 会保持对ue的强引用,因为us_PreviewKeyDown是静态的,而ue永远不会被回收。 - Daniel Bişar
@SACO 你能解释一下吗?"对ue的强引用"被保存在哪里?就我所看到的,John是完全正确的,原始示例中绝对没有内存泄漏。ue.PreviewKeyDown -= ue_PreviewKeyDown 不是必要的。 - Golvellius
@Golvellius 我发布了一个回答,应该能解释我的观点。我现在实际测试了一下,发现如果ue_PreviewKeyDown是静态的,就不会有泄漏问题。 - Daniel Bişar

5
我不同意DannySmurf的观点。
有些WPF布局对象在未被垃圾回收时会占用大量内存,导致应用程序变得很慢。所以,我认为使用“memory leak”一词是正确的,因为你正在泄漏不再使用的对象的内存。你期望这些项被垃圾回收,但它们没有被回收,因为某个地方仍有引用(在本例中是从事件处理程序中)。
现在给出一个真正的回答:)
我建议您阅读这篇 WPF性能文章MSDN

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

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

他们建议你研究弱事件模式
另一个解决方案是在使用完对象后删除事件处理程序。但我知道对于指向附加属性的情况可能不总是清晰的。
希望这可以帮到你!

3
我之所以投反对票是因为这个回答与实际问题无关。普遍人们都知道内存泄漏的危害和原因,尤其是由事件处理程序引起的。@Matt 想知道的是在使用附加行为时如何安全地处理事件处理程序。我很快会发布一个答案。 - Saraf Talukder

4

我知道在旧时代,内存泄漏是一个完全不同的主题。但是对于托管代码来说,将内存泄漏这个术语赋予新的含义可能更为合适...

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

为什么要实现弱事件模式?

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

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

我们在客户端应用程序中使用WPF,其中包含可以拖放的大型ToolWindows,所有这些都与XBAP兼容...但是我们在一些未被垃圾回收的ToolWindows上也遇到了同样的问题。这是由于它仍然依赖于事件侦听器...现在,当你关闭窗口并关闭应用程序时,这可能不是一个问题。但是,如果您创建了具有许多命令的非常大的ToolWindows,并且所有这些命令都会一遍又一遍地重新评估,并且人们必须整天使用您的应用程序...我可以告诉您...它真正阻塞了您的内存和应用程序的响应时间..

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


3

@Nick,关于附加行为的问题在于,按照定义,它们不在处理事件的元素所在的同一对象中。

我认为答案可能涉及到以某种方式使用WeakReference,但我没有看到任何简单的代码示例来说明它。 :)


2
为了解释我在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的引用。


感谢您投入时间回复我的评论,但是:John Fenton 在陈述一个硬引用存储在事件提供程序中时并没有错。通过将“a”设置为 null,您基本上正在删除这个“硬”引用,因为“a”指向的内存对象随后会被垃圾回收。而“a.Clicked += B.StaticHandleClicked;”正是 OP 的情况——这永远不会导致内存泄漏,因此 John Fenton 的问题是“那么...泄漏在哪里?”。只有你指出的反过来的情况才可能出现内存泄漏,但这不是 OP 遇到的情况。 - Golvellius

1

0
确保事件引用的元素在它们所引用的对象内部,如表单控件中的文本框。或者如果无法避免,请在全局帮助类上创建静态事件,然后监视全局帮助类以获取事件。如果这两个步骤都无法完成,请尝试使用弱引用,它们通常非常适合这些情况,但会带来一些额外开销。

0
我刚读了你的博客文章,我认为Matt给了你一些误导性的建议。如果这里真的有一个实际的内存泄漏,那么这是.NET Framework中的一个错误,而不是你可以必然地在你的代码中修复的东西。
我认为你(和你博客上的作者)在这里实际上谈论的不是泄漏,而是持续消耗内存。这不是同一件事。要明确的是,泄漏的内存是一种由程序保留,然后被放弃(即,指针悬空),其随后无法被释放的内存。由于内存是在.NET中管理的,理论上这是不可能的。但是,一个程序可以保留越来越多的内存,而不允许对它的引用超出范围(并变得有资格进行垃圾回收); 但是该内存并没有泄漏。一旦你的程序退出,GC将将其返回到系统。

好的,回答你的问题,我认为你实际上并没有问题。你肯定没有内存泄漏,从你的代码来看,我认为你不需要担心内存消耗。只要确保你不会重复分配事件处理程序而从未取消分配它(即,你只设置一次,或者每次分配它时都恰好删除它),你的代码应该没问题。

似乎这就是你博客上的帖子想给你的建议,但他使用了那个令人惊慌的词“泄漏”,这是一个可怕的词,但在托管世界中,许多程序员已经忘记了它的真正含义;它在这里不适用。


0

@Arcturus:

......当它们没有被垃圾回收时,会占用你的内存并使你的应用程序变得非常缓慢。

这是显而易见的,我不反对。然而:

......你正在泄漏内存到你不再使用的对象上......因为有一个引用指向它们。

“内存泄漏是指程序分配了一段内存后,由于设计错误,失去了对该内存的控制,从而造成系统的内存浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。”(维基百科,“内存泄漏”)

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

(抱歉要挑剔语义……也许我有点老派,但“泄漏”有一个非常具体的含义。人们现在倾向于使用“内存泄漏”来指代任何比他们想要的多消耗2KB内存的东西……)

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

在托管语言和非托管语言的上下文中,“泄漏”有着特定的含义。通常理解的术语是指在托管代码中,一个对象在内存中停留的时间超过了其预期寿命,被称为泄漏。您说得对,在非托管世界中,当应用程序未运行在受保护模式下时,未释放回操作系统的内存无法被回收,但这与OP的问题无关,并没有对讨论做出任何贡献。 - Anderson Imes

0

没错,你说得对。但是,有一整个新一代的程序员正在诞生,他们将永远不会接触非托管代码,我相信语言定义将一次又一次地重新发明自己。WPF中的内存泄漏与C / Cpp等方式不同。

当然,对于我的经理,我称之为内存泄漏... 对于我的同事,我则称之为性能问题!

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接