代码改变世界

【原】Silverlight内存泄漏原因、检测及解决(Memory Leak of Silverlight:Reason、Detection and Solving)

2013-03-10 17:29  拖鞋不脱  阅读(2404)  评论(0编辑  收藏  举报

在.Net 中,内存的管理和释放都由GC(Garbage Collection)管控,一般不需太多关注。但依然可能有内存泄漏(隐式内存泄漏)的情况出现,即一些早应结束它的生命周期的对象,直到程序结束才会被释放。具体到Silverlight中的表现,就是Silverlight所在浏览器进程占用的内存不断增长,直到崩溃或关闭浏览器。

In .Net world, GC manages the memory. So we normally don’t need too much concern over the memory mangement. But there still can be memory leak issue: some instances dont’t release memory until the end of application, while they have ended their life circle long before. Specific to the Silverlight situation, is the memory of the browser process who takes the silverlight continues to grow, until the collapsing or the closing of the browser.

内存泄漏产生的原因 (Reason of the Memory Leak)

对象是否释放是由GC管理的,什么样的对象会被GC释放?已经不再被应用程序的Root或者别的对象所引用的对象。而内存泄漏,就出现在那些从业务角度来看,应该已经不被引用,而实际仍被其他对象所引用的对象上。

假设有A、B两个对象,A引用了B,那么当A没有释放的时候,B也就不会被释放。如果A又恰巧是程序的Root或被Root所引用(如全局静态变量),那么B就很不幸的在整个程序的生命周期中都不会被释放。

这种情况一般不会发生,因为如果某个对象和Root产生了关联,往往是我们有意识的将它塑造成一个全局的、希望在整个程序的生命周期都存在的对象,这就不存在内存泄漏的问题。而意外往往就发生在我们无意识的地方——事件。

假设A有事件EventA,我们在B中处理了A的EventA,往往会写成这样:A.EventA += B.HandleEventA; 这时也许你不会意识到我们已经在A中添加了B的引用,因为在A中我们需要把B添加到它的监听者集合中,以便每次触发EventA的时候,都调用B的HandleEventA来处理这个事件。如果我们事后没有及时的通过A.EventA –= B.HandleEventA; 来将B从A的监听者集合中移除,那么B就会成为导致内存泄漏的一个元凶。

所以,全局事件监听、静态变量的事件监听,都可能在无意间消耗了我们大量的内存。

内存泄漏的检测 (Detection of the Memory Leak)

看到上面一节,也许你已经意识到在自己的应用中有这样的问题并准备着手解决。不过我依然想先提供两种检测内存泄漏的简单方案:

  1. 内存监控
    内存泄漏的表象就是应用占用的内存在不断增长而从不减少,这说明没有或很少有对象被释放了。如果你的Silverlight应用程序在每一次页面切换时都会增长内存占用,那么你的程序很可能就存在内存泄漏的问题。我们可以用任务管理器、Process Explorer或者VMMap等工具来监控Silverlight所在浏览器进程的内存增长情况,尤其是Private部分。
  2. 析构函数跟踪
    GC在释放对象时,总会调用它们的析构函数(Finalize方法),虽然一般不需要实现类的析构函数,但我们依然可以利用它来监控对象是否被释放。我们可以在析构函数中添加跟踪锚点(在Debug时添加断点,或者输出日志),来看看对象的析构函数是否被释放。如果断点始终没有命中或者日志从来什么也没有输出,那么恭喜你,你发现了内存泄漏的问题。

以上两种方法可以用来判断是否存在内存泄漏的问题,简单易行,而且结合对内存泄漏原因的理解,如果代码结构良好,逻辑清晰的话,应该很容易找到是哪里出现了不恰当的引用,从而解决内存泄漏的问题。但如果逻辑本身比较复杂,引用交织纷繁的话,我推荐利用Windbg进行更详细系统的排查,具体内容参见:http://blogs.msdn.com/b/delay/archive/2009/03/11/where-s-your-leak-at-using-windbg-sos-and-gcroot-to-diagnose-a-net-memory-leak.aspx

内存泄漏问题的解决 (Solving of the Memory Leak)

如同内存泄漏问题的检测有多种手段,内存泄漏问题的解决依然有多种方式:

  1. 直接移除事件监听。如上文所述,通过 A.EventA –= B.HandleEventA,直接移除了事。这种方式快捷明了,适用于有明确的生命起始、截止点的对象。如一个页面,我们可以把所有事件的监听(+=)放在页面的Loaded事件中,并在页面的UnLoaded事件中将事件监听移除,因为页面Unloaded说明页面已被移除并不会再启用。一些View甚至控件也可以用这种方式处理,只要我们确定,在UnLoaded之后,它们在业务上已经完成使命。
  2. 使用Weak Event Pattern(弱事件模式)。虽然直接移除比较干脆可控,但有些时候我们并无法确定一个对象真正的生命终结点。比如一个控件,当我们把它从当前View移除的时候就会触发它的UnLoaded事件,但我们不可以妄下结论说它就是生命终结了,也许使用者需要它在不可见的地方继续处理一些逻辑。而非View层的控制类,更无法在其内部判断它的起始、终结。我们可以通过让它们应用IDisposable接口,在Dispose方法中移除事件监听,并强制要求外部调用时,在合适的时候调用它的Dispose方法。但如果真的这么做,你会发现这可能会带来更多的问题——代码更复杂,耦合性更高,潜在问题更为隐蔽。
    所以微软推荐使用弱事件模式(Weak Event Patterns):http://msdn.microsoft.com/en-us/library/aa970850.aspx。原理就是将事件处理的B和触发事件的A之间的关联解除,使用中间件SomeEventWeakEventManager来调度,从而使A不会再直接引用B。
  3. WeakEventListener。事实上WeakEventListener就是Weak Event Pattern的一种实现,它利用了.Net提供的WeakReferrence类来很好的实现了引用管理。具体详见http://blogs.msdn.com/b/delay/archive/2009/03/09/controls-are-like-diapers-you-don-t-want-a-leaky-one-implementing-the-weakevent-pattern-on-silverlight-with-the-weakeventlistener-class.aspx,有说明有示例还有代码。值得一提的是,文下的评论(RScullard)中还提到了一种错误的用法,在做事件关联时,一定要保证OnEventAction和OnDetachAction是静态的,不要和当前上下文产生关联而引入额外的引用关系。

总结 (Summary)

所以我们要注意在Silverlight乃至.Net开发中的内存泄漏问题,尤其是全局事件或静态变量事件的处理,通过内存监控、析构函数跟踪等方法确定内存泄漏的发生,并通过Weak Event Pattern来规避此类问题的发生。

扩展阅读:http://blogs.msdn.com/b/delay/archive/2010/02/25/highlighting-a-weak-contribution-enhancements-make-preventing-memory-leaks-with-weakeventlistener-even-easier.aspx