.net中事件引發的內存泄漏分析

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

 

在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

Unnamed QQ Screenshot20151024202623

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

Unnamed QQ Screenshot20151024202709

咱們看到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的時候通常來講並不會關心內存泄漏的問題。

相關文章
相關標籤/搜索