在Winform和Asp.net時代,事件被大量的應用在UI和後臺交互的代碼中。看下面的代碼:html
private void BindEvent() { var btn = new Button(); btn.Click += btn_Click; } void btn_Click(object sender, EventArgs e) { MessageBox.Show("click"); }
這樣的用法能夠引發內存泄漏嗎?爲何咱們平時一直寫這樣的代碼歷來沒關注過內存泄漏?等分析完緣由後再來回答這個問題。架構
爲了測試緣由,咱們先寫一個EventPublisher類用來發布事件:asp.net
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的大小以方便咱們觀察內存使用量。ui
最後一步,寫一個簡單的winform程序,而後在一個Button的Click事件中寫入測試代碼:this
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次。.net
執行結果以下:orm
publisher和subscriber的數量都爲3,這並不表明發生了內存泄漏,只不過是沒有徹底回收完畢而已。每一個publisher在出了for循環後就會被認爲沒有任何用處,從而被正確回收。而註冊在上面的觀察者subscriber也能被正確回收。htm
再放一個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的時候通常來講並不會關心內存泄漏的問題。