2010-05-11 62 views
11

问题:已注册的事件处理程序将事件引用创建为事件处理程序的实例。如果该实例无法注销事件处理程序(大概是通过Dispose),那么实例内存将不会被垃圾收集器释放。使用WeakReference解决导致内存泄漏的.NET未注册事件处理程序的问题

例子:

class Foo 
    { 
     public event Action AnEvent; 
     public void DoEvent() 
     { 
      if (AnEvent != null) 
       AnEvent(); 
     } 
    }   
    class Bar 
    { 
     public Bar(Foo l) 
     { 
      l.AnEvent += l_AnEvent; 
     } 

     void l_AnEvent() 
     { 

     }    
    } 

如果我实例化一个Foo,并通过这一个新的酒吧构造,然后让Bar对象的旅途中,它不会被垃圾收集器释放,因为AnEvent登记。

我认为这是内存泄漏,看起来就像我的旧C++日子。当然,我可以让Bar IDisposable,在Dispose()方法中注销事件,并确保在其实例上调用Dispose(),但为什么我必须这样做?

我第一个问题,为什么事件与强引用?为什么不使用弱引用?事件用于抽象地通知对象另一个对象的变化。在我看来,如果事件处理程序的实例不再被使用(即没有对事件的非事件引用),那么它所注册的任何事件都应该自动取消注册。我错过了什么?

我看过WeakEventManager。哇,真是痛苦。使用起来不仅非常困难,而且其文档也不够充分(请参阅http://msdn.microsoft.com/en-us/library/system.windows.weakeventmanager.aspx - 注意到“继承者注释”部分,其中有6个模糊的项目符号)。

我在不同的地方看过其他讨论,但没有什么我觉得我可以使用的。如下所述,我提出了一个基于WeakReference的更简单的解决方案。我的问题是:这是否不符合要求,复杂程度要低得多?

class Foo 
    { 
     public WeakReferenceEvent AnEvent = new WeakReferenceEvent(); 

     internal void DoEvent() 
     { 
      AnEvent.Invoke(); 
     } 
    } 

    class Bar 
    { 
     public Bar(Foo l) 
     { 
      l.AnEvent += l_AnEvent; 
     } 

     void l_AnEvent() 
     { 

     } 
    } 

通知两件事情::

要使用的解决方案,如下上述代码被修改 1. Foo类被修改以两种方式:事件被替换WeakReferenceEvent的一个实例,如下所示;并且事件的调用被改变。 2. Bar类是UNCHANGED。

无需子类WeakEventManager的,实施IWeakEventListener等

好了,到WeakReferenceEvent实施。这显示在这里。需要注意的是它采用了通用的WeakReference <牛逼>,我从这里借:http://damieng.com/blog/2006/08/01/implementingweakreferencet

class WeakReferenceEvent 
{ 
    public static WeakReferenceEvent operator +(WeakReferenceEvent wre, Action handler) 
    { 
     wre._delegates.Add(new WeakReference<Action>(handler)); 
     return wre; 
    } 

    List<WeakReference<Action>> _delegates = new List<WeakReference<Action>>(); 

    internal void Invoke() 
    { 
     List<WeakReference<Action>> toRemove = null; 
     foreach (var del in _delegates) 
     { 
      if (del.IsAlive) 
       del.Target(); 
      else 
      { 
       if (toRemove == null) 
        toRemove = new List<WeakReference<Action>>(); 
       toRemove.Add(del); 
      } 
     } 
     if (toRemove != null) 
      foreach (var del in toRemove) 
       _delegates.Remove(del); 
    } 
} 

它的功能实在是微不足道。我重写operator +来获得+ =语法糖匹配事件。这会为Action委托创建WeakReferences。这允许垃圾收集器释放事件目标对象(在此示例中为Bar),当没有人持有该对象时。

在Invoke()方法中,只需运行弱引用并调用其目标操作即可。如果发现任何死亡(即垃圾收集)参考,请将其从列表中删除。

当然,这只适用于Action类型的代表。我试图做出这个通用的,但跑到失踪的地方T:委托在C#!

作为替代方案,只需修改类WeakReferenceEvent是一个WeakReferenceEvent <Ť>,和替换动作<Ť>中的作用。修复编译器错误,并且用户可以像这样被使用的类:

class Foo 
    { 
     public WeakReferenceEvent<int> AnEvent = new WeakReferenceEvent<int>(); 

     internal void DoEvent() 
     { 
      AnEvent.Invoke(5); 
     } 
    } 

与<牛逼>,和运营商的完整代码 - (去除事件)如下所示:

class WeakReferenceEvent<T> 
{ 
    public static WeakReferenceEvent<T> operator +(WeakReferenceEvent<T> wre, Action<T> handler) 
    { 
     wre.Add(handler); 
     return wre; 
    } 
    private void Add(Action<T> handler) 
    { 
     foreach (var del in _delegates) 
      if (del.Target == handler) 
       return; 
     _delegates.Add(new WeakReference<Action<T>>(handler)); 
    } 

    public static WeakReferenceEvent<T> operator -(WeakReferenceEvent<T> wre, Action<T> handler) 
    { 
     wre.Remove(handler); 
     return wre; 
    } 
    private void Remove(Action<T> handler) 
    { 
     foreach (var del in _delegates) 
      if (del.Target == handler) 
      { 
       _delegates.Remove(del); 
       return; 
      } 
    } 

    List<WeakReference<Action<T>>> _delegates = new List<WeakReference<Action<T>>>(); 

    internal void Invoke(T arg) 
    { 
     List<WeakReference<Action<T>>> toRemove = null; 
     foreach (var del in _delegates) 
     { 
      if (del.IsAlive) 
       del.Target(arg); 
      else 
      { 
       if (toRemove == null) 
        toRemove = new List<WeakReference<Action<T>>>(); 
       toRemove.Add(del); 
      } 
     } 
     if (toRemove != null) 
      foreach (var del in toRemove) 
       _delegates.Remove(del); 
    } 
} 

希望这会帮助别人,当他们遇到神秘事件导致垃圾收集世界的内存泄漏!

+0

“我第一个问题是为什么事件用强引用来实现?” - 在某些情况下,只有通过事件引用保持活动状态的中间对象以及发布者与订户之间的事件调用是非常有用的。例如,当您为每个事件源需要单独的实例时,这是一种有用的模式,并且您不知道在任何时间点将有多少事件源。 – 2010-05-11 23:09:22

+2

有用的文章,但没有太多的问题!你有没有考虑过这个博客呢? – 2010-05-11 23:15:51

+0

@Franci:好点。这是例外情况,与正常情况相反,你不觉得吗?我已经使用了相当多的事件,并且尚未需要此功能。 – Eric 2010-05-11 23:19:30

回答

2

我找到了我的问题的答案,为什么这不起作用。是的,的确,我错过了一个小细节:调用+ =来注册事件(l.AnEvent + = l_AnEvent;)会创建一个隐式的Action对象。该对象通常只由事件本身(以及调用函数的堆栈)保存。因此,当调用返回并且垃圾收集器运行时,隐式创建的Action对象被释放(现在只有弱引用指向它),并且事件未注册。

A(疼痛)的解决方案是保持到操作对象的引用如下:

class Bar 
    { 
     public Bar(Foo l) 
     { 
      _holdAnEvent = l_AnEvent; 
      l.AnEvent += _holdAnEvent; 
     } 
     Action<int> _holdAnEvent; 
     ... 
    } 

这工作,但是移除溶液的简单性。

2

当然这会对性能产生影响。

它有点像为什么在我的解决方案中引用其他程序集时,我可以使用反射动态读取程序集并在其类型中进行相关调用?

因此,在短期... 您使用的2个原因很强的参考... 1.类型安全(不是真的在这里适用) 2.性能

这又回到了仿制药的类似辩论通过哈希表。 上次我看到这个论点被放到桌面上,但是这张海报正在寻找一个预先生成的msil,或许这可以为您解决这个问题?

虽然还有一个想法...... 如果您附加了一个事件处理程序来说com对象事件会怎样? 从技术上讲,对象不被管理,所以如何知道什么时候需要清理,这当然归结为框架如何处理范围?

这篇文章自带的“它在我的头上garantee”,不承担责任为这个职位是如何描绘:)

0

有两种模式,我知道的制作弱事件订阅:一个是事件订阅者对指向他的代表有强烈的参照,而发布者对该代表的引用很弱。这具有需要通过弱参考来完成所有事件触发的缺点;它可能会使发布者不知道任何事件是否已过期。

另一种方法是让每个对某个对象真正感兴趣的人引用一个包装器,而该包装器又引用了“胆量”;事件处理程序只有对“胆量”的引用,并且胆量不包含对包装的强烈参考。包装器还保存对其Finalize方法将取消订阅事件的对象的引用(最简单的方法是使用一个简单的类,其Finalize方法调用操作<布尔型>值为False,并且其Dispose方法调用委托值为True并禁止最终确定)。

这种方法的缺点是需要通过包装器(一个额外的强引用)来完成主类上的所有非事件操作,但避免了除事件订阅和取消订阅之外的任何其他WeakReference。不幸的是,在标准事件中使用这种方法需要:(1)任何发布一个订阅事件的类必须具有线程安全的(最好是无锁的)'remove'处理器,可以从Finalize线程安全地调用(2)用于事件取消订阅的对象直接或间接引用的所有对象将保持半活动状态,直到终结器运行后GC通过。使用不同的事件范例(例如通过调用返回IDisposable的函数来订阅事件,可以使用它来取消订阅)可以避免这些限制。

0

当你有一个事件处理程序,你有两个对象:

  1. 类的对象。 (foo的一个实例)
  2. 表示您的事件处理程序的对象。 (例如,EventHandler的实例或Action的一个实例。)

您认为自己有内存泄漏的原因是EventHandler(或Action)对象内部持有对您的Foo对象的引用。这将阻止您的Foo对象被收集。

现在,你为什么不写一个WeakEventHandler?答案是可以,但你基本上确保:

  1. 你的委托(事件处理程序或行动的实例)从未有一个硬引用您的Foo对象
  2. 你WeakEventHandler持有强引用您的代理以及对您的Foo对象的弱引用
  3. 您有条款最终在弱引用变为无效时取消注册WeakEventHandler。这是因为没有办法知道何时收集对象。

实际上,这是不值得的。这是因为你必须权衡:

  • 你的事件处理方法将需要静态的,并采取了对象作为参数,这种方式不持有强引用您要收集的对象。
  • 您的WeakEventHandler和Action对象处于Gen 1或Gen 2的高风险状态。这会在垃圾回收器中造成高负载。
  • WeakReferences包含一个GC句柄。这可能会对性能产生负面影响。

因此,确保正确取消注册您的事件处理程序是一个更好的解决方案。语法更简单,内存使用更好,应用程序性能更好。