天天看點

.net中事件引起的記憶體洩漏分析

系列主題:基于消息的軟體架構模型演變

在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次。

執行結果如下:

.net中事件引起的記憶體洩漏分析

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不能垃圾回收。這次的執行結果如下:

.net中事件引起的記憶體洩漏分析

我們看到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/

聲明:本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。