系列主題:基于消息的軟體架構模型演變
在Winform和Asp.net時代,事件被大量的應用在UI和背景互動的代碼中。看下面的代碼:
private void BindEvent()
{
var btn = new Button();
btn.Click += btn_Click;
}
void btn_Click(object sender, EventArgs e)
{
MessageBox.Show("click");
}
這樣的用法可以引起記憶體洩漏嗎?為什麼我們平時一直寫這樣的代碼從來沒關注過記憶體洩漏?等分析完原因後再來回答這個問題。
為了測試原因,我們先寫一個EventPublisher類用來釋出事件:
public class EventPublisher
{
public static int Count;
public event EventHandler<PublisherEventArgs> OnSomething;
public EventPublisher()
{
Interlocked.Increment(ref Count);
}
public void TriggerSomething()
{
RaiseOnSomething(new PublisherEventArgs(Count));
}
protected void RaiseOnSomething(PublisherEventArgs e)
{
EventHandler<PublisherEventArgs> handler = OnSomething;
if (handler != null) handler(this, e);
}
~EventPublisher()
{
Interlocked.Decrement(ref Count);
}
}
這個類提供了一個事件OnSomething,另外在構造函數和析構函數中分别會對變量Count進行累加和遞減。Count的數量反應了EventPublisher的執行個體在記憶體中的數量。
寫一個Subscriber用來訂閱這個事件:
public class Subscriber
{
public string Text { get; set; }
public List<StringBuilder> List = new List<StringBuilder>();
public static int Count;
public Subscriber()
{
Interlocked.Increment(ref Count);
for (int i = 0; i < 1000; i++)
{
List.Add(new StringBuilder(1024));
}
}
public void ShowMessage(object sender, PublisherEventArgs e)
{
Text = string.Format("There are {0} publisher in memory",e.PublisherReferenceCount);
}
~Subscriber()
{
Interlocked.Decrement(ref Count);
}
}
Subscriber同樣用Count來反映記憶體中的執行個體數量,另外我們在構造函數中使用StringBuilder開辟1000*1024Size的大小以友善我們觀察記憶體使用量。
最後一步,寫一個簡單的winform程式,然後在一個Button的Click事件中寫入測試代碼:
private void btnStartShortTimePublisherTest_Click(object sender, EventArgs e)
{
for (int i = 0; i < 100; i++)
{
var publisher = new EventPublisher();
publisher.OnSomething += new Subscriber().ShowMessage;
publisher.TriggerSomething();
}
MessageBox.Show(string.Format("There are {0} publishers in memory, {1} subscribers in memory", EventPublisher.Count, Subscriber.Count));
}
for循環中的代碼是一個很普通的事件調用代碼,我們将Subscriber執行個體中的ShowMessage方法綁定到了publisher對象的OnSomething事件上,為了觀察記憶體的變化我們循環100次。
執行結果如下:

publisher和subscriber的數量都為3,這并不代表發生了記憶體洩漏,隻不過是沒有完全回收完畢而已。每個publisher在出了for循環後就會被認為沒有任何用處,進而被正确回收。而注冊在上面的觀察者subscriber也能被正确回收。
再放一個Button,并在Click中寫以下測試代碼:
private void BtnStartLongTimePublisher_Click(object sender, EventArgs e)
{
for (int i = 0; i < 100; i++)
{
var publisher = new EventPublisher();
publisher.OnSomething += new Subscriber().ShowMessage;
publisher.TriggerSomething();
LongLivedEventPublishers.Add(publisher);
}
MessageBox.Show(string.Format("There are {0} publishers in memory, {1} subscribers in memory", EventPublisher.Count,Subscriber.Count));
}
這次for循環中不同之處在于我們将publisher儲存在了一個list容器當中,進而保證100個publisher不能垃圾回收。這次的執行結果如下:
我們看到100個subscribers全部儲存在記憶體中。如果觀察資料總管中的記憶體使用率,你也能發現記憶體突然漲了幾百兆并且再不會減少。
想一下下面的場景:
public class Runner
{
private LongTimeService _service;
public Runner()
{
_service = new LongTimeService();
}
public void Run()
{
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
}
}
LongTimeService是一個長期運作的服務,從來不被銷毀,這将導緻所有注冊在SomeThingUpdated 事件上的觀察者也不會能回收。當有大量的觀察者不停的注冊在SomeThingUpdated 上時,就會發生記憶體洩漏。
這三個測試說明了引起事件記憶體洩漏的場景:當觀察者注冊在了一個生命周期長于自己的事件主題上,觀察者不能被記憶體回收。
解決辦法是在事件上顯示調用-=符号。
再回過頭來看開始提出來的問題:當使用了Button的Click事件的時候,會發生記憶體洩漏嗎?
btn.Click += btn_Click;
觀察者是誰?btn_Click方法的擁有者,也就是Form執行個體。
主題是誰?Button的執行個體btn
主題btn什麼時候銷毀?當Form執行個體被銷毀的時候。
當Form被銷毀的時候,btn及其觀察者都會被銷毀。除非Form從來不銷毀,并且大量的觀察者持續注冊在了btn.Click上才能發生記憶體洩漏,當然這種場景是很少見的。是以我們開發winform或者asp.net的時候一般來說并不會關心記憶體洩漏的問題。
作者:Richie Zhang
來源:http://www.cnblogs.com/richieyang/
聲明:本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。