事件处理程序泄漏已经存在很长时间了,这是 WPF (Windows Presentation Foundation)开发人员经常要处理的最麻烦的问题之一。您可能会想:是什么让事件处理程序泄漏如此重要?事件处理程序泄漏很容易引起,只需忘记取消订阅事件即可。此外,它们很难发现,甚至更难修复。
在更新17.9预览1中引入的 Visual Studio 托管内存使用工具中添加的新见解大大简化了发现/修复这些泄漏的过程。它提供有关哪些对象正在泄漏及其订阅的事件的信息。
什么是事件处理程序泄漏?
当对象在功能上无法使用时,没有被识别为垃圾收集时,它会在堆上泄漏。这意味着它将无意中在内存中保持活动状态。事件处理程序因导致这种情况而臭名昭著。这是因为事件处理程序在对象和它订阅的事件之间创建了一个直接引用。
在本例中,我们有一个 Publisher 和 Subscriber 类。当 Subscriber 调用 Subscribe 时,MyEvent 将在堆中链接 Publisher 和 Subscriber:
这样做的问题是,如果 Subscriber 忘记取消订阅,这些引用将保留在堆上,从而泄漏 Subscriber。这里最简单的解决方案是简单地调用 Unsubscribe 方法,但是在更复杂的应用程序中,很难跟踪对象的订阅以及何时退订。这就是 Memory Analyzer 可以帮助开发人员解决这些问题的地方。
好吧,让我们看看如何解决它?
为了证明这一点,我们将对一个示例 WPF 应用程序进行演练调试,以查找事件处理程序泄漏:
在本例中,我们有一个窗口,它在打开时订阅 dispatcherTimer_Tick 事件。事件在做什么并不重要。这段代码的重要部分是,当窗口关闭时,我们忘记取消订阅事件:
这里取消订阅语句的注释是有问题的,因为它在窗口关闭时不再正确地取消订阅,并且会导致泄漏。为了找到它,让我们开始调试这个应用程序(F5)。为简单起见,让我们假设 main() 正确地导致泄漏,它打开 AdWindow,导致它订阅一个事件,然后关闭它。
首先,我们需要打开诊断工具窗口。要在调试会话中访问它,请执行 Debug -> Windows -> Diagnostic Tools。
当它在调试会话中打开时,它应该是这样的:
此窗口显示正在调试的应用程序的堆的总体大小和 CPU 百分比。当我们单击相机图标拍摄快照时,我们可以通过单击 Objects 下的值来查看堆并访问事件处理程序泄漏见解。
进入快照的堆视图后,导航到 Insights(见解):
我们终于到站了!在 Insights 选项卡中,我们可以看到泄漏事件处理程序的列表,并且我们可以看到泄漏窗口的显示。此外,我们可以看到该泄漏所浪费的总量。仅仅是这个简单的例子就造成了 4.93 KB 的泄漏!这是因为窗口有一个完整的子树,它引用的对象也会因为忘记取消订阅而泄漏。
此外,您可以通过点击"Show Just My code"来过滤掉所有的系统代码,只显示 AdWindow。
这很酷,但现在怎么办?
到目前为止,我们已经成功地确定了我们的应用程序中的泄漏。现在,如果我们想要修复它,我们可以点击"View Details"来查看有关问题的更多信息,更重要的是,如何修复它。
这个视图向我们展示了一些关于泄漏的关键信息。我们可以看到对象的地址,它持有的事件处理程序,最重要的是,它订阅的对象。这传达了要解决这个问题,AdWindow 必须从 DispatcherTimer 取消订阅。此外,您还可以看到 AdWindow 对象的引用图。"Referenced Objects"选项卡显示了由于 AdWindow 而泄漏的额外对象的数量。
我们还能做什么?
如果您已经读到这里了,你可能会对这种见解的其他用途感兴趣。例如,检测逻辑可以处理任何类型的事件处理程序。考虑前面的带有发布者和订阅者类的控制台应用程序,如果我们创建自己的事件处理程序,我们仍然可以检测到它。
告诉我们您的想法!
在未来的版本中,这种体验还会有很多改进。请下载最新的 Visual Studio 预览版并提供您的反馈。请在 Visual Studio 中通过"Report a Problem"或直接在开发者社区站点提出问题并提供反馈。