脱钩后,C#仍然挂钩了一个事件

时间:2011-05-26 15:25:01

标签: c# events memory-leaks hook

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

然而,我刚遇到类似的问题。有一个名为WorkItem(短期)的类,它在构造函数中注册到另一个名为ClientEntityCache(long long)的类的事件。这些事件从来没有被解开,我可以在.NET Profiler中看到WorkItem的实例在不应该因为这些事件而保持活着状态。所以我决定让WorkItem实现IDisposable,在Dispose()函数中我以这种方式取消事件:

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

修改

以下是我用于订阅的代码:

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

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

编辑结束

我在使用WorkItem的代码中的适当位置调用了Dispose,当我调试时,我可以看到函数真正被调用,而且我做 - =对于每个事件。但是我仍然得到内存泄漏,我的WorkItems在Disposed之后仍然存活,在.NET Profiler中我可以看到实例保持活动,因为事件处理程序(如EntityCacheClearedEventHandler)仍然在它们的调用列表中有它们。我试图解开它们不止一次(倍数= =)只是为了确保它们不会被连接多次,但这没有帮助。

任何人都知道为什么会这样或者我能做些什么来解决问题? 我想我可以更改事件处理程序以使用弱委托,但这需要用大量遗留代码来搞乱。

谢谢!

编辑:

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

5 个答案:

答案 0 :(得分:4)

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

// 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)

取消事件时,它需要是同一个委托。像这样:

public class Foo
{
     private MyDelegate Foo = ClientEntityCache_CacheCleared;
     public void WorkItem()
     {
         ClientEntityCache.EntityCacheCleared += Foo;
     }

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

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

public class Foo
{
     public void WorkItem()
     {
         ClientEntityCache.EntityCacheCleared +=
new MyDelegate(ClientEntityCache_CacheCleared);
     }

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

所以-=并没有取消你订阅的原始版本,因为它们是不同的代表。

答案 2 :(得分:0)

也许试试:

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

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

删除对 原始 订阅事件方法的引用,删除活动订阅。

您可以随时设置eventhandler = delegate {};在我看来,这会比null更好。

答案 3 :(得分:0)

你是否正在解开正确的参考?使用-=取消挂钩时,不会产生任何错误,如果您正在解除未挂钩的事件,则不会发生任何错误。但是,如果使用+=添加,如果事件已被挂钩,则会出现错误。现在,这只是一种诊断问题的方法,但是尝试添加事件,如果你 DONT 得到错误,问题是你用错误的引用解开事件。

答案 4 :(得分:0)

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

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