您的位置:首页 > 其它

解决事件会引起内存泄漏的问题:Weak Event Handlers

2010-04-02 16:40 597 查看

解决事件相关的问题: Weak Event Handlers

问题回放

如果使用不当,代理可能会造成内存泄漏。例如,我们为一个事件添加了处理函数,却忘记了移除该处理函数,而且声明事件的那个对象生命周期长于处理函数所在的对象,这时内存将会泄漏。见下图。



在上图中,对象“eventExposer”声明了事件“SpecialEvent”。然后对象“myForm”在该事件上添加了一个处理函数。myForm被关闭后,我们希望它被垃圾回收,但是实际上却没有。潜在事件上挂接的代理对象仍然维持了一个myForm的强引用, 因为挂接到事件上的处理函数没有被移除。
那么,我们怎么解决这个问题呢?
大多数人会说,修改Form类啊, 你个笨蛋。移除处理器不就好了吗!这看上去很合理-特别是我们对代码有完全的控制权。但是如果我们没有这种控制权呢?例如我们的程序是一个很大的基于插件的架构,而且事件是由某些事件服务提供,并且生命周期跟整个应用程序一样。而且Form是一个第三方的插件,我们没有代码。这种情况下,我们就造就了一个内存泄漏的应用程序。

有一些解决方案

理想的解决方案是在CLR级别上创建一些弱代理,这些弱代理持有目标对象,而不是直接强引用对象。不幸的是,这种机制并不存在,而且在可预见的将来也不大可能存在。所以我们还得自己来。

眼前的障碍

我必须承认,在创建一个理想的解决方案面前,有很多障碍。首先,我们不能简单的从System.Delegate或者System.MulticastDelegate继承并且重写某些方法。看上去非常好,但是却行不通。你甚至不能用IL做到这点。此外代理没有一个泛型约束。
然而最大的障碍就是性能,有两个性能关注点需要我们考虑:1添加/移除一个处理函数。2真正调用处理函数。一般来说,调用比添加和删除处理函数更频繁, 所以我们将会重点关注第二点。有很多方法可以动态调用一个代理,与常规的代理调用来说,这些方法性能低下。使用weak delegate 和weak event会有一些额外开销,但是我们应该可以将性能开销降到最低。
另一个潜在的严重问题就是移除处理函数。让我来解释,当我们使用一个magic类来包装事件处理器并且将其附加到一个事件。我们创建了一个到包含处理函数所在对象的弱引用,但是事件仍然包含一个对包装对象的强引用。所以,当目标对象被垃圾回收以后,包装对象仍然在内存中,而且不能被垃圾回收。这看上去就是用一个耗费内存少的对象来取代一个耗费内存多的对象。但是内存泄漏的事情仍然存在。
我想提的最后一个障碍是如何创建一个可以处理所有代理类型的类。如果我们可以使用一个特别的代理类型,我可以建立一个可以处理任何代理而且能够获取高性能的解决方案。基于这点,我将集中关注.NET 2.0中提供的System.EventHandler<TEventArgs>代理类型。

第一次尝试

下面先看看这个magic类。这是一个非常天真的实现
using System;
using System.Reflection;

public class WeakDelegate
{
private WeakReference m_TargetRef;
private MethodInfo m_Method;

public WeakDelegate(Delegate del)
{
m_TargetRef = new WeakReference(del.Target);
m_Method = del.Method;
}

public object Invoke(params object[] args)
{
object target = m_TargetRef.Target;

if (target != null)
return m_Method.Invoke(target, args);
}
}
这种方法实际上只会在非常简单的场景下工作,但是当与事件一起应用的时候却不是那么健壮。但这个类却阐述了如何使用WeakReference类对代理目标创建弱引用。实例化一个WeakReference并传递你想要跟踪的对象。然后就可以使用WeakReference.IsAlive属性来判断目标对象是否被垃圾回收。谨记:千万不要下面的代码
if (m_TargetRef.IsAlive) // race condition!!!!
return m_Method.Invoke(m_TargetRef.Target, args);
我看过见太多的开发者犯了这种错误。问题在于垃圾回收器运行在另一个线程之上。这会在调用m_TargetRef.IsAlive 和 m_TargetRef.Target之间造成竞赛条件。换句话说,m_TargetRef.IsAlive可能返回true,但是垃圾回收器这时挤进来并且在你调用m_TargetRef.Target之前将它进行了回收。这很可能引起NullReferenceException异常被抛出。正确的做法是,使用一个局部变量在存储m_TargetRef.Target,然后对其进行为空判断,因为局部变量维持了一个对目标对象的强引用,保证该对象可以被安全使用。
可见,使用WeakReference相当简单,但是有一个需要着重指出的问题是:WeakReference对象有一个终结器,却没有实现IDisposable接口。这意味着什么?这意味着每一个WeakReference实例都会对垃圾回收器增加小部分压力(参见GC.AddMemoryPressure),因为WeakReference实例在被回收之前,它的终结器必须被调用。在内部,WeakReference使用System.GCHandle来跟踪目标对象并且在终结器中清理GCHandle。如果WeakReference实现了IDisposable,那么我们可以在使用完成后调用Dispose方法。这可以释放GCHandle并调用GC.SuppressFinalize(this)来保证终结器不会被调用(这会移除GC压力-参见GC.RemoveMemoryPressure)。现在我们可以使用GCHanlde来代替使用WeakReference。但是这么做有一个问题,GCHandle需要LinkDemand权限来保证非托管代码安全权限。这意味着我们的WeakDelegate需要相同的权限。WeakReference实际上通过调用GCHandle的内部方法绕开了这些安全限制。因为WeakReference需要较少的权限检查,所以使用它将会获取性能上的提高。
下面还有一些需要指出的问题,在我们实现中,代理是用MethodInfo.Invoke()调用的,这种调用性能低下。直接调用代理会更快,但是因为代理保留了对目标对象的一个强引用。所以我们不能存储一个对原始代理的引用(译者注:我们也不能存储一个原始代理的弱引用,那样的话,这个代理将会立刻被垃圾回收)。这就是我们放弃封装System.Delegate而去封装System.EventHandler<TEventArgs>原因。
通过使用WeakDelegate具有多播功能,可以提高本解决方案的健壮性。当前,我们只支持单代理,如果与事件协作使用,这是不够的。解决方案是添加静态的Combine和Remove方法,这些方法等价于System.Delegate上声明的方法。做完这些以后,我们就可以按照如下方式使用弱事件了。
public class EventProvider
{
private WeakDelegate m_WeakEvent;

protected virtual void OnWeakEvent(EventArgs e)
{
if (m_WeakEvent != null)
m_WeakEvent.Invoke(this, e);
}

public event EventHandler WeakEvent
{
add
{
m_WeakEvent = WeakDelegate.Combine(m_WeakEvent, value);
}
remove
{
m_WeakEvent = WeakDelegate.Remove(m_WeakEvent, value);
}
}
}
这里主要的问题是所有的处理器都是弱引用,无论是不是你真的需要。如果WeakDelegate类还能像常规代理那样来使用,那么灵活性将会大大提高。这么做后,一个订阅者可以指定一些处理器是弱引用,而其他则不是。订阅者理想的语法应该如下:
public class EventProvider
{
public event EventHandler MyEvent;
}
public class EventSubscriber
{
public EventSubscriber(EventProvider provider)
{
provider.MyEvent += new WeakDelegate(MyWeakEventHandler);
}

private void MyWeakEventHandler(object sender, EventArgs e)
{
}
}

第二次尝试

基于这一点,我打算提供一个封装任何System.Delegate的解决方案,做了大量的工作(当然是作者做的)后,我发现这是一个让我哭的像小孩一样的解决方案。显然,我们要创建一个快速的轻量级的解决方案,这条路是行不通了。
好了,我打算放弃创建一个能够接受任何代理类型的magic类。现在集中精力到一个特别的代理类型System.EventHandler<TEventArgs>,这将会将我们的解决方案限制在2.0+上,不过这应该不是个问题。
目的明确了,创建一个magic WeakEventhandler类也就不是问题了。下面就是我们期望的语法:
using System;
using System.Reflection;

public class WeakEventHandler<E>
where E: EventArgs
{
private WeakReference m_TargetRef;
private MethodInfo m_Method;
private EventHandler<E> m_Handler;

public WeakEventHandler(EventHandler<E> eventHandler)
{
m_TargetRef = new WeakReference(eventHandler.Target);
m_Method = eventHandler.Method;
m_Handler = Invoke;
}

public void Invoke(object sender, E e)
{
object target = m_TargetRef.Target;
if (target != null)
m_Method.Invoke(target, new object[] { sender, e });
}

public static implicit operator EventHandler<E>(WeakEventHandler<E> weh)
{
return weh.m_Handler;
}
}
这个版本看上去就是我们想要的。我们明确的指导我们方法的签名并且能像使用正常的代理那样使用我们的假代理。多亏了隐式转换语法,我们才能写这样的代码。
public class EventProvider
{
public event EventHandler<EventArgs> MyEvent;
}
public class EventSubscriber
{
public EventSubscriber(EventProvider provider)
{
provider.MyEvent += new WeakEventHandler<EventArgs>(MyWeakEventHandler);
}

private void MyWeakEventHandler(object sender, EventArgs e)
{
}
}
我运行了一个简单的测试来添加和触发100次事件处理器,一个用正常的方法,一个用WeakEventHandler,下面就是结果:
Added 100 normal listeners to notifier: 0.000289 seconds.
Added 100 weak listeners to notifier: 0.002701 seconds.
Fired 100 normal listeners: 0.000263 seconds.
Fired 100 weak listeners: 0.001531 seconds.
显然,这个方法非常缓慢,通过MethodInfo.Invoke()来调用是正常调用时间的6倍。
这留给我们两个问题去解决:
1. 调用性能,我们使用MethodInfo.Invoke(),他性能低下。
2. 目标被垃圾回收后,移除WeakEventHandler对象
第二个问题明显要比第一个问题简单

挽救性能

使用MethodInfo.Invoke如此缓慢,所以我们更希望能够有一个代理,不用存储对目标的引用,而是当我们调用的时候再指定目标。这样的话,我们得到的性能就能跟标准调用不相上下了。不过这些方案都是基于.Net2.0+。(作者第一次尝试使用了lightweight code generation (LCG)来解决性能问题,可是结果却是得其反,所以在此我们就不说这些试探性的方案了。)
第二个解决方法是使用开放实例代理(open instance delegate),那么什么是开发实例代理呢?它运行在运行的时候动态指定目标对象。为了做到这点,开放实例代理必须声明一个额外的参数用来接收目标对象。
你很可能没有听说过什么是开放实例代理,因为C#和VB都不支持它们。为了创建一个开放实例代理,你需要调用Delegate.CreateDelegate方法的一个重载。开放实例代理是设计用来支持STL.NET and C++/CLI的。
下面是使用开放实例代理的修改后的代码:
using System;

public class WeakEventHandler<E>
where E: EventArgs
{
private delegate void OpenEventHandler(object @this, object sender, E e);

private WeakReference m_TargetRef;
private OpenEventHandler m_OpenHandler;
private EventHandler<E> m_Handler;

public WeakEventHandler(EventHandler<E> eventHandler)
{
m_TargetRef = new WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null, eventHandler.Method);
m_Handler = Invoke;
}

public void Invoke(object sender, E e)
{
object target = m_TargetRef.Target;

if (target != null)
m_OpenHandler(target, sender, e);
}

public static implicit operator EventHandler<E>(WeakEventHandler<E> weh)
{
return weh.m_Handler;
}
}
编译成功,但是根本不起作用,Delegate.CreateDelegate方法抛出System.ArgumentException,错误消息是:目标方法绑定错误。通过查阅MSDN,发现开放实例代理的第一个参数应该跟处理函数所在对象的类型匹配。为了改正这个问题,我们需要在WeakEventHandler类上添加泛型参数来表示处理函数所在对象的类型。下面是新的版本:
using System;

public class WeakEventHandler<T, E>
where T: class
where E: EventArgs
{
private delegate void OpenEventHandler(T @this, object sender, E e);

private WeakReference m_TargetRef;
private OpenEventHandler m_OpenHandler;
private EventHandler<E> m_Handler;

public WeakEventHandler(EventHandler<E> eventHandler)
{
m_TargetRef = new WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null, eventHandler.Method);
m_Handler = Invoke;
}

public void Invoke(object sender, E e)
{
T target = (T)m_TargetRef.Target;

if (target != null)
m_OpenHandler(target, sender, e);
}

public static implicit operator EventHandler<E>(WeakEventHandler<T, E> weh)
{
return weh.m_Handler;
}
}
这次,代码工作良好,但是我们客户代码却不是那么优雅了。因为我们必须指定第二个泛型参数:
public class EventSubscriber
{
public EventSubscriber(EventProvider provider)
{
provider.MyEvent += new WeakEventHandler<EventSubscriber, EventArgs>(MyWeakEventHandler);
}

private void MyWeakEventHandler(object sender, EventArgs e)
{
}
}
虽然额外的泛型参数令人非常不爽,但是性能问题确实得到了解决。

自动取消注册

理想的情况是,当目标被垃圾回收的时候,我们WeakEventHandler会自动被从事件中移除。不幸的是,这时不可能的。因为目标被垃圾回收的时候,我们没有得到任何通知。所以我们需要传递给WeakEventHandler的构造函数一个代理,我们将会在invoke方法中判断如果目标被垃圾回收,那么该代理将会被调用。代码如下:
using System;

public class WeakEventHandler<T, E>
where T: class
where E: EventArgs
{
public delegate void UnregisterCallback(EventHandler<E> eventHandler);

private delegate void OpenEventHandler(T @this, object sender, E e);

private WeakReference m_TargetRef;
private OpenEventHandler m_OpenHandler;
private EventHandler<E> m_Handler;
private UnregisterCallback m_Unregister;

public WeakEventHandler(EventHandler<E> eventHandler, UnregisterCallback unregister)
{
m_TargetRef = new WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null, eventHandler.Method);
m_Handler = Invoke;
m_Unregister = unregister;
}

public void Invoke(object sender, E e)
{
T target = (T)m_TargetRef.Target;

if (target != null)
m_OpenHandler(target, sender, e);
else if (m_Unregister != null)
{
m_Unregister(m_Handler);
m_Unregister = null;
}
}

public static implicit operator EventHandler<E>(WeakEventHandler<T, E> weh)
{
return weh.m_Handler;
}
}
如果我们传递一个UnregisterCallback代理给WeakEventHandler,那么它将在WeakEventHandler被调用而且目标被垃圾回收的时候调用。这解决了我们的问题。不过如果WeakEventHandler一直未被调用,那么WeakEventHandler对象将会一直存在,不过我想这是可以接受的。现在客户代码看上去如下所示:
public class EventSubscriber
{
public EventSubscriber(EventProvider provider)
{
provider.MyEvent += new WeakEventHandler<EventSubscriber, EventArgs>(MyWeakEventHandler,
delegate(EventHandler<EventArgs> eh)
{
provider.MyEvent -= eh;
});
}

private void MyWeakEventHandler(object sender, EventArgs e)
{
}
}
现在我们有了一个magic解决方案,下面让我们看看如何提高一点语法,让她用起来更简单。

让她漂亮一点

最重要的事情是摆脱掉那个我们加入的额外泛型参数。它需要我们过多思考才能正确使用。我们要做的是把它转移到另一个方法中。这次我们要求助于反射来构造WeakEventHandler<T, E>对象了。代码如下:
public delegate void UnregisterCallback<E>(EventHandler<E> eventHandler)
where E: EventArgs;

public interface IWeakEventHandler<E>
where E: EventArgs
{
EventHandler<E> Handler { get; }
}

public class WeakEventHandler<T, E>: IWeakEventHandler<E>
where T: class
where E: EventArgs
{
private delegate void OpenEventHandler(T @this, object sender, E e);

private WeakReference m_TargetRef;
private OpenEventHandler m_OpenHandler;
private EventHandler<E> m_Handler;
private UnregisterCallback<E> m_Unregister;

public WeakEventHandler(EventHandler<E> eventHandler, UnregisterCallback<E> unregister)
{
m_TargetRef = new WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null, eventHandler.Method);
m_Handler = Invoke;
m_Unregister = unregister;
}

public void Invoke(object sender, E e)
{
T target = (T)m_TargetRef.Target;

if (target != null)
m_OpenHandler.Invoke(target, sender, e);
else if (m_Unregister != null)
{
m_Unregister(m_Handler);
m_Unregister = null;
}
}

public EventHandler<E> Handler
{
get { return m_Handler; }
}

public static implicit operator EventHandler<E>(WeakEventHandler<T, E> weh)
{
return weh.m_Handler;
}
}

public static class EventHandlerUtils
{
public static EventHandler<E> MakeWeak<E>(EventHandler<E> eventHandler, UnregisterCallback<E> unregister)
where E: EventArgs
{
if (eventHandler == null)
throw new ArgumentNullException("eventHandler");
if (eventHandler.Method.IsStatic || eventHandler.Target == null)
throw new ArgumentException("Only instance methods are supported.", "eventHandler");

Type wehType = typeof(WeakEventHandler<,>).MakeGenericType(eventHandler.Method.DeclaringType, typeof(E));
ConstructorInfo wehConstructor = wehType.GetConstructor(new Type[] { typeof(EventHandler<E>),
typeof(UnregisterCallback<E>) });

IWeakEventHandler<E> weh = (IWeakEventHandler<E>)wehConstructor.Invoke(
new object[] { eventHandler, unregister });

return weh.Handler;
}
}
可以看出,我们提取了IWeakEventHandler<E>接口,没有它的话,我们需要使用反射访问m_Handler字段。
到此为止,我希望我们的性能仍然良好。下面是用同一个测试方法测试的性能数据,如下:
Added 100 normal listeners to notifier: 0.000298 seconds.
Added 100 weak listeners to notifier: 0.011509 seconds.
Fired 100 normal listeners: 0.000288 seconds.
Fired 100 weak listeners: 0.000745 seconds.
WeakEventHandler类稍慢了一些,这是因为它引发了两次代理调用。如果缓存构造函数,并且使用动态方法来调用构造函数,那么性能还是有一些提升。
我们已经改进了语法,现在调用起来很简单:
public class EventSubscriber
{
public EventSubscriber(EventProvider provider)
{
provider.MyEvent += EventHandlerUtils.MakeWeak<EventArgs>(MyWeakEventHandler,
delegate(EventHandler<EventArgs> eh)
{
provider.MyEvent -= eh;
});
}

private void MyWeakEventHandler(object sender, EventArgs e)
{
}
}
C#3.0后,我们可以用lambda表达式来代替匿名方法:
public class EventSubscriber
{
public EventSubscriber(EventProvider provider)
{
provider.MyEvent += EventHandlerUtils.MakeWeak<EventArgs>(MyWeakEventHandler,
eh => provider.MyEvent -= eh);
}

private void MyWeakEventHandler(object sender, EventArgs e)
{
}
}
然后我们把MakeWeak方法转变为EventHandler<TEventArgs>的一个扩展方法,现在代码看上去如下所示:
public class EventSubscriber
{
public EventSubscriber(EventProvider provider)
{
provider.MyEvent += new EventHandler<EventArgs>(MyWeakEventHandler).MakeWeak(eh => provider.MyEvent -= eh);
}

private void MyWeakEventHandler(object sender, EventArgs e)
{
}
}
如果你想让客户使用弱处理函数订阅,你可以如此编码:
public class EventProvider
{
private EventHandler<EventArgs> m_MyEvent;
public event EventHandler<EventArgs> MyEvent
{
add
{
m_Event += value.MakeWeak(eh => m_Event -= eh);
}
remove
{
}
}
}
以下参考未完成
GC.AddMemoryPressure参考
GCHandle参考
WeakReference参考
后期绑定参考
SecurityAction参考
Reflection Token API参考
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: