読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

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

MVVM WPF C#
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 StatusViewModel(Person model)
        : base(model)
    {
    }

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

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