MVVMパターンで陥りやすいメモリリークについて考えてみた

先日id:okazukiさんが
■PrismでViewよりViewModelのライフサイクルが長い時メモリリーク起きそう - かずきのBlog@Hatena
の記事で、PrismInteractionRequestが実装方法によってはメモリリークが発生する危険性をはらんでいることを見つけて下さった件をきっかけに、MVVMパターンWPFSilverlightをやる上でメモリリークについて気をつけとくポイントなどまとめとこうかなと思った。

MVVMパターンが(僕の中で)メジャーになる前から言われてたことだけど、

    1. どっかの息の長い配列に追加したまま忘れてた
    2. どっかのイベント・デリゲートの参照を解除し忘れていた

等々、オブジェクトがどこから参照されてるかを把握しきれてないことが原因になることが多い。
別にMVVMパターンじゃなくても気をつけなきゃならないのだけれど、とくに2番に関してはMVVMパターンで実装してるとうっかり起こってしまう危険性が高いように思う。(当社比)

  • なぜMVVMパターンで実装してるとうっかり起こってしまう危険性が高いように思えるの?

思うに、
自分で+=したら覚えてるので-=しなきゃいけないって思うけど、データバインディング機構使ってると自分で+=しないことが多いから
が大きい理由な気がする。(当社比)

とくに最初のPrismのInteractionRequestの例のように、フレームワーク内部でイベント発行・購読をやっていると気づきにくい。
便利さにうかれてつい・・・。

※ ここでPrismのInteractionRequestでどうやったらメモリリークするのかの説明を少し。
下記のような理由で、実装によってはメモリリークになってしまう。
 ・InteractionRequestはRaiseイベントを購読させる。
 ・リスナーとなるInteractionRequestTriggerはBlendSDKのSystem.Windows.Interactivity.EventTriggerを継承している。
 ・System.Windows.Interactivity.EventTriggerは強参照でSourceObjectのEventにAddHandlerしてしまうので、SourceObjectがEventTriggerよりも寿命が長い場合、EventTriggerは残り続けてしまう。
 ・例えば、ViewにEventTrigger、ViewModelにInteractionRequestを配置したとして、ViewModelは保存しといてViewだけ閉じちゃおうと思っても、Viewは閉じられた後も実は参照が残っているので生き続けてしまうというようなことが起こる。

即席で作ったんですが悪い実装でメモリリークするサンプルは下記。
SubWindowを開いて閉じて、GC.CollectをしてもCollectされない。(簡単な説明でスミマセン)
https://skydrive.live.com/?cid=bc790e45968a1da9&sc=documents&move=BC790E45968A1DA9!110&sid=BC790E45968A1DA9!109&iscopy=0&id=BC790E45968A1DA9%21111#

もともとSystem.Windows.Interactivity.EventTriggerはView内要素のEventを想定しているのかなと思ったりしたけど、実装を調べてみるとSouceObjectが変化するとちゃんとRemoveHandlerしてくれるので結局はViewを破棄するときにSouceObjectをnullにすればよいだけかなとも思ったり。

  • 気をつけるべきパターンは?

自分で+=しないやつ全般なんだけど、とくに嵌りやすいのは下記だろう。

    1. Viewだけ破棄して、ViewModelは保存しておきたいとき。
    2. ListBox、DataGridなどでVirtualizingPanelによって項目が仮想化されるとき

とくに2は最悪で、VirtualizationMode.Standardになってると際限なく項目のインスタンスが作成されていって死にます。
(逆に言うと、Recyclingにしとけば問題なくね?って話になったりもする)
(もひとつ逆に言うと、派手にメモリリークしてくれた方が気づきが早くて良いと思ったりもする。だって2の場合は「オブジェクトがどこから参照されてるかを把握しきれてないことが原因」じゃなくて、「オブジェクトが勝手に消されたり生成されてたりしたなんて知りませんでしたよ」なんですから。早く教えてくれた方が良い。)

  • 対策は?

WeakEvent パターンなど、弱参照を検討するのはもちろんのことなんだけど、SilverlightにはWeakEventManager無いし、そもそもListenerにこのパターンを適用していないView要素があるのが問題だったりするのでその他を考えてみた。

気をつけるべきパターンの1に関しては最初は「ViewとViewModelのライフサイクルを合わせるような設計にすべきなのかな」とか思ってたんだけど、最近考えていることは
「Viewが消えて欲しいときに必ずView.DataContext = nullとするように実装すればいいだけのことなんじゃなかろうか」
っていうこと。
どういうことかというと、今まで見てきたライブラリ内部で強いイベント参照をしている部分は必ず「OnSourceChanged」みたいなメソッドがあり、イベントソースの変更を監視して変更された場合はRemoveHandler(もしくは-=)をやっているようだから。
(考えてみれば、ソースが変更されたのにRemoveHandlerしなければソースが変更されるたびにHandlerが呼ばれる回数が増えていくし、当たり前と言えば当たり前の実装であるけど)
(逆に言うとWeakEventにもせず、ソース変更でRemoveHandlerもしないListenerの実装があったらバグに近いような気もする・・・)

そもそもデータバインディングという機構で密接な関係にあるVとVMのライフサイクルを変えたいわけなんだから、ライフサイクルを変えるときに密接な関係も終わりにしてあげるのが二人のためのような気がする。
もともとデータバインディング関係なしの時はライフサイクルの違うオブジェクト同士だと+=したら-=しなきゃいけなかったんだし。
DataContext = nullでも消えてくれないような未練たらたらなListenerオブジェクトがある時のみ、弱参照を考えるでも遅くないかな。

気をつけるべきパターンの2は、ちょっと簡単すぎていいんだろうかと思うんだけど
VirtualizationModeをRecycleにする
で解決できちゃうと思う。
Listenerが消えないのでリークしようが無くなる。
Silverlightに関してはデフォルトがRecycleみたいだし、Standardにしようと頑張って試してみたんだけど無理だった。
(調べ方が悪いかもしれないのでStandardにするやり方をご存じの方是非教えて欲しい。。。)

  • まとめ

メモリリークってなんだか凄いバグのような気がするけど、ようするに「生き残れ」って命令してるから生き残ってるだけだから、生き残って欲しくなかったら「なくなれ」って命令すれば良いだけのことなのでそんなに怖くない・・・と思えるようになりました最近。













・・・でもまだ気づいてないリークがあなたの後ろに・・・(ホラー映画かよw)