かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

MVVMでメモリリークしちゃってました 原因と対策編

2011/03/02 
コメントの指摘を受けて修正。
間違ってた部分。
WeakPropertyChangedViewModelBaseクラスの以下の処理。
    PropertyChangedEventManager.AddListener(
        this.Model,
        new PropertyChangedWeakEventListener(
            base.RaisePropertyChanged),
        string.Empty);
正しいものに差し替えました。taguoさん指摘ありがとうございました。

今、UxeenというWPF製でMVVM適用して頑張って作ってるTwetterクライアントのプロジェクトのリーダーのid:anis774さんにプロジェクトに入れてもらってMVVM関連のところを見たり、気になるところに手を入れたりしてる今日この頃です。


そこで、こんな感じのプログラムを書いていたのですよ。

namespace WpfApplication1
{
    using System.ComponentModel;

    // INotifyPropertyChangedの実装クラス
    class NotificationObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            var h = this.PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    // Modelクラス
    class Person : NotificationObject
    {
        private string name;
        public string Name
        {
            get
            {
                return this.name;
            }

            set
            {
                this.name = value;
                base.RaisePropertyChanged("Name");
            }
        }
    }

    // PersonのViewModel
    class PersonViewModel : NotificationObject
    {
        private Person model;
        public PersonViewModel(Person model)
        {
            this.model = model;
            this.model.PropertyChanged += ModelPropertyChanged;
        }

        public string Name
        {
            get
            {
                return this.model.Name;
            }

            set
            {
                this.model.Name = value;
            }
        }

        private void ModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // ModelとViewModelのプロパティ名一緒だからそのまま使おう
            base.RaisePropertyChanged(e.PropertyName);
        }
    }
}

このプログラムの特性としてModelは結構長期間にわたってメモリ上に保持されるけど、ViewModelはModelに比べて短命だというのがあります。この条件が成り立つとき以下のようなことが起きてViewModelのインスタンスがいつまでたってもGCに回収されないという動きをして、結果としてメモリリークが起きていました。


流れとしては以下のような感じです。

  • ViewModelのコンストラクタでModelのイベントを購読する
    • this.model.PropertyChanged += ModelPropertyChanged;
    • ここでModelがViewModelに対する強い参照を保持する
  • ViewModelの変数が誰からも参照されない状態になる
  • しかしModelはViewModelをしっかり参照している
  • ViewModelはいつまでたってもGCに回収されない
  • 結果メモリリーク\(^o^)/

弱いイベントパターン

Weak Event Patternというのが、こういう問題を解決するソリューションとして提供されています。MSDNの以下のページです。

この弱いイベントパターンの実装例ってあまりなくて、日本語の情報では、以下のサイトを見つけました。

ということで、上記のサイトとMSDNとPrism内部でも使われてたので、それらを参考にして以下のように修正してみました。

// ModelのPropertyChangedを弱いイベントパターンでリッスンする
class WeakPropertyChangedViewModelBase<TModel> : NotifyPropertyChangedBase
    where TModel : INotifyPropertyChanged
{
    protected TModel Model { get; private set; }

    // イベントのリスナ保持のためのフィールド
    private IWeakEventListener propertyChangedListener;

    public WeakPropertyChangedViewModelBase(TModel model)
    {
        this.Model = model;
        // ここが超ポイント!!
        // IWeakEventListenerを作成してEventManagerに渡す。
        // このときListenerのインスタンスは、フィールドなどで管理して
        // ViewModelがGCの対象になるまで破棄されないようにする
        this.propertyChangedListener = new PropertyChangedWeakEventListener(
                base.RaisePropertyChanged);
        PropertyChangedEventManager.AddListener(
            this.Model,
            this.propertyChangedListener,
            string.Empty);
    }
    
    // IWeakEventListenerの実装
    class PropertyChangedWeakEventListener : IWeakEventListener
    {
        private Action<string> raisePropertyChangedAction;

        public PropertyChangedWeakEventListener(Action<string> raisePropertyChangedAction)
        {
            this.raisePropertyChangedAction = raisePropertyChangedAction;
        }

        public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            // PropertyChangedEventManagerじゃないと処理しない
            if (typeof(PropertyChangedEventManager) != managerType)
            {
                return false;
            }

            // PropertyChangedEventArgsじゃないと処理しない
            var evt = e as PropertyChangedEventArgs;
            if (evt == null)
            {
                return false;
            }

            // コンストラクタで渡されたコールバックを呼び出す
            this.raisePropertyChangedAction(evt.PropertyName);
            return true;
        }
    }
}

このような、クラスを用意してViewModelクラスの実装を以下のような感じにします。

class PersonViewModel : WeakPropertyChangedViewModelBase<Person>
{

    public PersonViewModel(Person model)
        : base(model)
    {
    }

    public string Name
    {
        get
        {
            return base.Model.Name;
        }
        
        set
        {
            base.Model.Name = value;
        }
    }
}

こうすることで、ViewModelは必要なくなったら心置きなく成仏できるようになりました。めでたしめでたし。ってかハマルねこれは。