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

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

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

有帮助吗?

解决方案

我不同意丹尼·斯莫夫

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

现在真正的答案:)

我建议你读一下这篇文章 MSDN 上的 WPF 性能文章

不删除对象上的事件处理程序可能会使对象活着

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

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

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

希望这可以帮助!

其他提示

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

ue.PreviewKeyDown += ue_PreviewKeyDown;

硬引用 ue_PreviewKeyDown 存储在 ue.PreviewKeyDown.

ue_PreviewKeyDown 是一个 STATIC 方法并且不能是 GCed.

没有硬性参考 ue 正在被存储,所以没有什么可以阻止它被存储 GCed.

所以...哪里漏了?

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

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

为什么要实现 WeakEvent 模式?

聆听事件会导致内存泄漏。聆听事件的典型技术是使用将处理程序附加到源上的事件的语言特定语法。例如,在C#中,该语法是:source.someevent +=新的EventHandler(MyEventHandler)。

该技术从事件源到事件侦听器创建了强有力的参考。通常,为听众附加事件处理程序会导致听众具有受源对象寿命影响的对象寿命(除非事件处理程序明确删除)。但是在某些情况下,您可能希望听众的对象寿命仅由其他因素控制,例如当前是否属于应用程序的视觉树,而不是由源的寿命。每当源对象寿命延伸超出侦听器的对象寿命时,正常事件模式就会导致内存泄漏:听众保持比预期更长的时间。

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

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

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

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

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

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

为了解释我对约翰·芬顿帖子的评论,这是我的回答。我们来看下面的例子:

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 的引用。

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

我刚刚读了你的博文,我认为你得到了一些误导性的建议,马特。如果有一个 实际的 记忆 泄露 如果是的话,那么这是 .NET Framework 中的一个错误,并且不一定可以在代码中修复。

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

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

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

@大角星:

...堵塞您的内存,并在未收集垃圾时使应用程序真正缓慢。

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

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

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

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

(很抱歉成为一个语义纳粹......也许我有点老派,但泄漏有一个非常具体的含义。如今,人们倾向于使用“内存泄漏”来表示任何消耗超过他们想要的 2KB 内存的情况......)

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

真实真实,

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

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

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

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

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

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

反正。无意将其变成关于语言的不切实际的讨论。只是说...

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top