【问题标题】:Preventing Memory Leaks with Attached Behaviours使用附加行为防止内存泄漏
【发布时间】:2010-09-06 01:21:59
【问题描述】:

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

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

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

【问题讨论】:

    标签: .net wpf memory


    【解决方案1】:

    我不同意 DannySmurf

    某些 WPF 布局对象可能会阻塞您的内存,并在未进行垃圾回收时使您的应用程序变得非常缓慢。所以我发现单词的选择是正确的,你正在将内存泄漏给你不再使用的对象。您希望这些项目被垃圾收集,但事实并非如此,因为某处存在引用(在本例中是来自事件处理程序)。

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

    我建议你阅读这篇WPF Performance article on MSDN

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

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

    他们建议您查看Weak Event pattern

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

    希望这会有所帮助!

    【讨论】:

    • 我投了反对票,因为答案与提出的实际问题无关。一般来说,每个人都知道内存泄漏的坑及其原因,特别是由于您提到的事件处理程序。 @Matt 这里想知道的是如何在使用内部附加行为时安全地处理事件处理程序。我会尽快发布答案。
    【解决方案2】:

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

    ue.PreviewKeyDown += ue_PreviewKeyDown;
    

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

    ue_PreviewKeyDown 是STATIC 方法,不能是GCed。

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

    那么...泄漏在哪里?

    【讨论】:

    • 这是一个常见的误解。 ue.PreviewKeyDown += ue_PreviewKeyDown 保持对 ue 的强引用,因为 us_PreviewKeyDown 是静态的,永远不会收集 ue。
    • @SACO 你能解释一下吗? “对 ue 的强引用”保存在哪里?据我所知,John 是完全正确的,原始示例中绝对没有内存泄漏。 ue.PreviewKeyDown -= ue_PreviewKeyDown 不是必需的。
    • @Golvellius 我发布了一个答案,应该可以解释我的观点。我现在实际测试了一下,发现如果 ue_PreviewKeyDown 是静态的,就不会泄漏。
    【解决方案3】:

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

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

    为什么要实现 WeakEvent 模式?

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

    这种技术创造了一个强大的 从事件源引用到 事件监听器。通常情况下,附加 侦听器的事件处理程序导致 听者有一个对象 受对象影响的寿命 源的生命周期(除非 事件处理程序被显式删除)。 但在某些情况下,您可能 想要对象的生命周期 监听器只能由 其他因素,例如是否 当前属于视觉树 的应用程序,而不是由 源的生命周期。每当 源对象的生命周期超出 侦听器的对象生命周期, 正常的事件模式导致 内存泄漏:监听器被保留 比预期的寿命更长。

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

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

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        为了解释我对 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 所面临的情况。
        【解决方案6】:

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

        1. Weak Event Pattern in WPF
        2. Weak Event Patterns (MSDN)

        【讨论】:

          【解决方案7】:

          确保事件引用元素位于它们所引用的对象中,例如表单控件中的文本框。或者,如果这无法避免。在全局助手类上创建一个静态事件,然后监视全局助手类的事件。如果这两个步骤无法完成,请尝试使用 Wea​​kReference,它们通常非常适合这些情况,但它们会带来开销。

          【讨论】:

            【解决方案8】:

            我刚刚阅读了您的博文,我认为您得到了一些误导性的建议,Matt。如果此处存在实际内存泄漏,则这是 .NET Framework 中的错误,而不是您必须在代码中修复的问题。

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

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

            这似乎是你博客上的发帖人试图给你的建议,但他使用了那个令人震惊的工作“泄漏”,这是一个可怕的词,但许多程序员已经忘记了托管的真正含义世界;此处不适用。

            【讨论】:

              【解决方案9】:

              @Arcturus:

              ...堵塞你的记忆,让你的 应用程序真的很慢 不是垃圾收集。

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

              ...您正在向对象泄漏内存 你不再使用...因为 有他们的参考。

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

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

              (对不起,我是一个语义纳粹……也许我有点老派,但泄漏有一个非常具体的含义。现在人们倾向于使用“内存泄漏”来表示任何消耗 2KB 内存超过他们想要...)

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

              【讨论】:

              • 泄漏在托管语言与非托管语言的上下文中具有特定含义。一般理解的命名法是指一个对象留在内存中的时间超过其预期寿命,作为托管代码中的泄漏。您说得对,在非托管世界中,当应用程序未在保护模式下运行时,无法回收未释放回操作系统的内存,但这与 OP 的问题无关,对讨论没有任何帮助。
              【解决方案10】:

              确实如此,

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

              或者对我的经理来说,我将其称为内存泄漏。对我的同事,我将其称为性能问题!

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

              【讨论】:

                【解决方案11】:

                嗯,(经理位)我当然可以理解和同情。

                但无论微软如何称呼它,我认为“新”定义并不合适。这很复杂,因为我们并不生活在一个 100% 托管的世界中(尽管微软喜欢假装我们这样做,但微软本身并不生活在这样一个世界中)。当您说内存泄漏时,您可能意味着程序消耗了太多内存(这是用户的定义),或者在退出之前不会释放托管引用(如此处),或者未正确清理非托管引用up(这将是真正的内存泄漏),或者从托管代码调用的非托管代码正在泄漏内存(另一个真正的泄漏)。

                在这种情况下,“内存泄漏”的含义很明显,尽管我们并不精确。但是与某些人交谈会变得非常乏味,他们称每一次过度消费或未能收集到内存泄漏;当这些人是程序员时令人沮丧,他们应该知道得更好。我认为,技术术语具有明确的含义很重要。当他们这样做时,调试会容易得多。

                无论如何。不要打算把它变成一个关于语言的空想的讨论。只是说...

                【讨论】:

                  猜你喜欢
                  • 2010-09-21
                  • 1970-01-01
                  • 2015-02-05
                  • 1970-01-01
                  • 1970-01-01
                  • 2019-10-17
                  • 2021-10-22
                  • 1970-01-01
                  相关资源
                  最近更新 更多