取消挂钩后 C# 仍然挂钩到事件

C# still hooked to an event after unhooking

我目前正在调试一个包含内存泄漏的大(非常大!)C# 应用程序。它主要使用 Winforms 作为 GUI,尽管有几个控件是在 WPF 中制作并由 ElementHost 托管。到目前为止,我发现许多内存泄漏是由于事件没有被取消挂钩(通过调用 -=)引起的,我已经能够解决问题。

但是,我刚刚遇到了类似的问题。有一个名为 WorkItem(短期)的类,它在构造函数中注册到另一个名为 ClientEntityCache(长期)的类的事件。这些事件从未解开,我可以在 .NET 分析器中看到,由于这些事件,WorkItem 的实例在它们不应该保持活动状态时保持活动状态。所以我决定让 WorkItem 实现 IDisposable 并在 Dispose() 函数中以这种方式解开事件:

1
2
3
4
5
public void Dispose()
{
  ClientEntityCache.EntityCacheCleared -= ClientEntityCache_CacheCleared;
  // Same thing for 10 other events
}

编辑

这是我用于订阅的代码:

1
2
3
4
5
public WorkItem()
{
  ClientEntityCache.EntityCacheCleared += ClientEntityCache_CacheCleared;
  // Same thing for 10 other events
}

我还将取消注册的代码更改为不调用新的 EntityCacheClearedEventHandler。

编辑结束

我在使用 WorkItem 的代码中的适当位置调用了 Dispose,当我调试时,我可以看到该函数确实被调用了,并且我对每个事件都执行了 -=。但是我仍然会遇到内存泄漏,并且我的 WorkItems 在 Disposed 后仍然保持活动状态,并且在 .NET 分析器中我可以看到实例保持活动状态,因为事件处理程序(如 EntityCacheClearedEventHandler)仍然将它们放在调用列表中。我试图不止一次地解开它们(多个-=),只是为了确保它们没有被多次钩住,但这没有帮助。

有人知道为什么会发生这种情况,或者我可以做些什么来解决这个问题吗?
我想我可以更改事件处理程序以使用弱委托,但这需要用一大堆遗留代码搞砸很多。

谢谢!

编辑:

如果这有帮助,这里是 .NET profiler 描述的根路径:
很多东西都指向ClientEntityCache,它指向EntityCacheClearedEventHandler,它指向Object[],它指向另一个EntityCacheClearedEventHandler实例(我不明白为什么),它指向WorkItem。


可能是多个不同的委托函数连接到事件。希望下面的小例子能让我更清楚我的意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Simple class to host the Event
class Test
{
  public event EventHandler MyEvent;
}

// Two different methods which will be wired to the Event
static void MyEventHandler1(object sender, EventArgs e)
{
  throw new NotImplementedException();
}

static void MyEventHandler2(object sender, EventArgs e)
{
  throw new NotImplementedException();
}


[STAThread]
static void Main(string[] args)
{
  Test t = new Test();
  t.MyEvent += new EventHandler(MyEventHandler1);
  t.MyEvent += new EventHandler(MyEventHandler2);

  // Break here before removing the event handler and inspect t.MyEvent

  t.MyEvent -= new EventHandler(MyEventHandler1);      
  t.MyEvent -= new EventHandler(MyEventHandler1);  // Note this is again MyEventHandler1    
}

如果您在删除事件处理程序之前中断,您可以在调试器中查看调用列表。见下文,有 2 个处理程序,一个用于 MyEventHandler1,另一个用于方法 MyEventHandler2.

enter image description here

现在删除 MyEventHandler1 两次后,MyEventHandler2 仍然被注册,因为只剩下一个委托它看起来有点不同,它不再显示在列表中,但是直到 MyEventHandler2 的委托被删除它仍然会被引用由事件。

enter image description here


取消挂钩事件时,它必须是同一个委托。像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Foo
{
     private MyDelegate Foo = ClientEntityCache_CacheCleared;
     public void WorkItem()
     {
         ClientEntityCache.EntityCacheCleared += Foo;
     }

     public void Dispose()
     {
         ClientEntityCache.EntityCacheCleared -= Foo;
     }
}

原因是,您使用的是语法糖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Foo
{
     public void WorkItem()
     {
         ClientEntityCache.EntityCacheCleared +=
new MyDelegate(ClientEntityCache_CacheCleared);
     }

     public void Dispose()
     {
         ClientEntityCache.EntityCacheCleared -=
new MyDelegate(ClientEntityCache_CacheCleared);
     }
}

所以 -= 不会取消与您订阅的原始用户的挂钩,因为它们是不同的代表。


如果实例被事件处理程序保持活动状态,则 GC 不会调用 Dispose,因为它仍然被事件源引用。

如果您自己调用 Dispose 方法,则引用将超出范围。


您是否取消了正确的参考?当您使用 -= 取消挂钩时,不会产生错误,并且如果您取消未挂钩的事件,则不会发生任何事情。但是,如果您使用 += 添加,并且该事件已被挂钩,则会收到错误消息。现在,这只是您诊断问题的一种方法,但请尝试添加事件,如果您没有收到错误,则问题是您使用错误的引用取消了事件。


也许可以试试:

1
2
3
4
5
 public void Dispose()
    {
      ClientEntityCache.EntityCacheCleared -= ClientEntityCache_CacheCleared;
      // Same thing for 10 other events
    }

您正在创建一个新的事件处理程序并将其从 delegate 中删除 - 这实际上什么都不做。

通过移除对原始订阅事件方法的引用来移除事件订阅。

你总是可以设置你的 eventhandler = delegate {}; 在我看来,这会比 null 更好。