11

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

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

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

4

11 回答 11

5

我不同意丹尼蓝精灵

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

现在寻求真正的答案:)

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

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

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

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

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

希望这可以帮助!

于 2008-08-18T08:39:35.507 回答
5

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

ue.PreviewKeyDown += ue_PreviewKeyDown;

硬引用ue_PreviewKeyDown存储在ue.PreviewKeyDown.

ue_PreviewKeyDown是一种STATIC方法,不能是GCed

没有硬引用ue被存储,所以没有什么能阻止它成为GCed.

所以...泄漏在哪里?

于 2011-05-29T18:56:32.340 回答
4

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

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

为什么要实现 WeakEvent 模式?

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

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

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

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

于 2008-08-18T14:44:49.763 回答
3

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

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

于 2008-08-18T01:51:27.593 回答
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 的引用。

于 2015-01-30T15:33:53.160 回答
1

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

  1. WPF 中的弱事件模式
  2. 弱事件模式 (MSDN)
于 2008-08-18T08:48:25.473 回答
0

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

于 2008-08-18T00:58:04.283 回答
0

我刚读了你的博文,我认为你得到了一些误导性的建议,马特。如果这里确实存在内存泄漏那么这是 .NET Framework 中的错误,而不是您必须在代码中修复的问题。

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

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

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

于 2008-08-18T03:40:18.403 回答
0

@大角星:

...当它们没有被垃圾收集时,阻塞你的内存并使你的应用程序非常慢。

这很明显,我不反对。然而:

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

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

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

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

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

于 2008-08-18T14:27:48.127 回答
0

真真切切,

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

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

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

于 2008-08-18T15:20:39.800 回答
-2

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

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

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

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

于 2008-08-18T15:09:55.577 回答