MVVMパターンで陥りやすいメモリリークについて考えてみた
先日id:okazukiさんが
■PrismでViewよりViewModelのライフサイクルが長い時メモリリーク起きそう - かずきのBlog@Hatena
の記事で、PrismのInteractionRequestが実装方法によってはメモリリークが発生する危険性をはらんでいることを見つけて下さった件をきっかけに、MVVMパターンでWPFとSilverlightをやる上でメモリリークについて気をつけとくポイントなどまとめとこうかなと思った。
- どうやったらメモリリークするの?
MVVMパターンが(僕の中で)メジャーになる前から言われてたことだけど、
-
- どっかの息の長い配列に追加したまま忘れてた
- どっかのイベント・デリゲートの参照を解除し忘れていた
等々、オブジェクトがどこから参照されてるかを把握しきれてないことが原因になることが多い。
別に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にすればよいだけかなとも思ったり。
- 気をつけるべきパターンは?
自分で+=しないやつ全般なんだけど、とくに嵌りやすいのは下記だろう。
-
- Viewだけ破棄して、ViewModelは保存しておきたいとき。
- 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)
前にコードレシピにあげたRxのサンプルへのありがたいご指摘のまとめ
以前にコードレシピにあげたサンプル
■Reactive Extensionsを使用してTwitterから非同期にデータを取得し表示する
について、なんとMicrosoft MVPでRxの日本での開拓者(と僕が思っている)neueさんがブログで取りあげてくださって、よりよい書き方についてご指摘下さった。
(ブログ記事自体は僕のサンプルのことがメインではなく、Rxを使っての再帰的な辿り方やAsyncCTPとの連携など幅広く、深い記事です。是非、最後まで読んでみてください)
■非同期の再帰的な辿り方、或いはRxとC# 5.0 Asyncの連携について
サンプルをあげた時のブログにも書いたけど、てさぐり状態だったのでneueさんからのご指摘は大変ありがたかった。
で、そのまとめ
- (引用)単純なものは演算子の組み合わせで、複雑なものは素直に偉大なるコンパイラ生成(yield return)に頼る。そういう切り分けがLINQ的には大事
- 今思えば、もっと素直に頭を使えば良かった。(つまり、yieldを使えば良かった) Modelを考えるとき、TwitterAPIの引数と戻り値の形式にこだわってしまったのが失敗だったかなあと。大事なことはTwitterAPIに形式を合わせることではなく、結果(Friends)をできるだけ取得しやすいように取得することなんだから、Jsonの形に合わせたUsersResultで返すより、IEnumerable
の方が扱いやすいのは至極当たり前のことだった。
- 今思えば、もっと素直に頭を使えば良かった。(つまり、yieldを使えば良かった) Modelを考えるとき、TwitterAPIの引数と戻り値の形式にこだわってしまったのが失敗だったかなあと。大事なことはTwitterAPIに形式を合わせることではなく、結果(Friends)をできるだけ取得しやすいように取得することなんだから、Jsonの形に合わせたUsersResultで返すより、IEnumerable
- (引用)IEnumerable
とIObservable に両対応できてる、という柔軟性 - 前述の通りyieldを使いIEnumerable
として返しておけば、その処理を非同期で(バックグラウンドで)行って結果を取得したい場合はToObservableで良い。「同期的か、非同期かは呼び出し側が決めれば良く、その切り替え(同期的取得or非同期での取得)も呼び出し側で簡単にできる」と言うことは非常に重要(というかもともとIOvservable がIEnumerable との対称性を意識して作られているのはそのことが主目的だろうし...)だと改めてご指摘を受けて考え直した、どうも知らないメソッド(この場合はGenerate)の使い方を考えるのに一生懸命で、本質的なことを見失っていたよう。木を見て森を見ずとはこのことか。
- 前述の通りyieldを使いIEnumerable
- もうざっくり「非同期」ってゆうのやめよう
僕のサンプルに関してのまとめは以上。
てさぐり状態の中でとても勉強になりました。
neueさんには大変感謝しています。
あと、neueさんの記事にはAsyncCTPとの連携についても書かれていて、Asyncについても追っていかなきゃなと思った。
がんばれ俺。
KinkumaFramework 1.2.1.12 がリリースされました!
id:okazukiさんがKinkumaFramework 1.2.1.12をリリースしてくださいました。
http://okazukimvvmsupport.codeplex.com/releases/view/68994
NuGetでも「Kinkuma」と入力すればでてきます。
最近僕が追加したExtendedInvokeCommandActionが追加されています。
(■Kinkuma Frameworkに機能追加したよ。)
ぜひ試してみてください!
RxサンプルのFriends・Followers両方バージョン作ってみた
コードレシピに上げたやつを少し改良。
こんどはコードレシピじゃなくて、Codeplexにした。
■Rxを使って非同期でデータを取得するサンプル
http://asyncgetlistsample.codeplex.com/releases/view/68960
使い方はユーザー名を入れてEnter押すだけ。
Friendsだけだとあまりにもショボイかなと思ったので、Followersも取れるようにした。
作ってて思ったけど、FriendsとFollowersをメンテさせるアプリってあんまないかも。
発展させてちょっとしたツールにしてみようかしら・・・
ただ、Deprecated resoucesのTwitterApi使ってるのがちょっと。
だっていちいち認証したくないんだもん。
Observable.Generateメソッド3回目 〜終了判定に使った要素も欲しいんですが・・・〜
またまたObservable.Generateメソッドの続き。
前回、コードレシピにサンプルを登録したんだけども(Reactive Extensionsを使用してTwitterから非同期にデータを取得し表示する)、見返してて、というか作ってるうちから自分的にイテてないな〜と思う部分があった。
それは、Observable.Generateメソッドの終了判定でViewModelのFriendsコレクションに結果を追加しているところ。
var o = Observable.Generate( new UsersResult { next_cursor = "-1", users = new List<User>() }, i => { // 終了判定の中でFriendsコレクションに追加するのが非常に気持ち悪い Friends.AddRange(i.users); return i.next_cursor != "0"; }, i => { return client.GetFriends(i.next_cursor); }, i => i, Scheduler.ThreadPool); o.Subscribe( i => { // できればここでFriendsコレクションに追加したい }, ...................
終了判定は終了判定だから、終了判定以外のことはなるべくさせたくない。
Subscribeメソッドの中でできたら良いんだけども・・・。
そもそもなぜ終了判定の中で追加したかっていうと、
Observable.Generateメソッドの引数の呼ばれ方が
var o = Observable.Generate( 1, i => { return i < 5; // 1.終了判定 }, i => { return ++i; // 4.次を呼ぶ処理 }, i => { return i; // 2.セレクタ } ).Subscribe( i => { Console.WriteLine(i); // 3.OnNext } );
のようになっているからで、
- 終了判定が呼ばれ、trueだった場合のみ
- セレクタが呼ばれ、そこで返したものが
- OnNextで受け取れる
というものだから。
つまり、「1.終了判定」以外でないと、終了判定がfalse(サンプルの例で言えばnext_cursorが"0")となった結果は受け取れないのだ。
OnNextは「次の反復処理を呼ぶ前に実行されるもの」という意味だから、当たり前だけど。
じゃあ、終了判定に使った結果は、終了判定以外でどうやって受け取るの?
と調べたけど、どうもできないらしい。(調べ足りないかもなので、できるかた教えてほしい)
コールバックも引数なしだし・・・。
ということで苦肉の策。
var o = Observable.Generate( new UsersResult { next_cursor = "-1", users = new List<User>() }, i => { //終了判定はnullかどうか return i != null; }, i => { //next_cursorが"0"の場合、Apiを呼ばないでnullを返す return i.next_cursor == "0" ? null : client.GetFriends(i.next_cursor); }, i => i, Scheduler.ThreadPool) .Subscribe( i => Friends.AddRange(i.users) );
うーん、まだちょっとイケてない気するけど、前のよりはマシかも。
Observable.Generateの続き(コードレシピにサンプルあげた)
前回の記事の続き。
いくらRxでも非同期でもConsoleに数字出して何が嬉しいんじゃーってことで
Twitterがら非同期でデータを取得して表示するWPFアプリのサンプル作った。
■Reactive Extensionsを使用してTwitterから非同期にデータを取得し表示する
http://code.msdn.microsoft.com/Reactive-ExtensionsTwitter-b8238b51
肝の部分のコードはこんな感じ
var client = new TwitterAccessClient(ScreenName); //GenerateメソッドをScheduler.ThreadPoolとともに呼び出すことによって、 //非同期にFriendsを取得します。 var o = Observable.Generate( new UsersResult { next_cursor = "-1", users = new List<User>() }, i => { //ここで取得した結果をFriendsに追加します。 Friends.AddRange(i.users); //次のカーソルが0の場合、終了します。 return i.next_cursor != "0"; }, i => { //次のカーソルを引数にしてFriends取得メソッドを実行します。 return client.GetFriends(i.next_cursor); }, i => i, //Scheduler.ThreadPoolを指定することで別スレッドで実行されます。 Scheduler.ThreadPool); o.Subscribe( i => { }, //エラー this.HadleError, () => { isBusy = false; //情報テキストを変更します。 base.RaisePropertyChanged(() => InformationText); });
だけど、もっと良い書き方あるかも。あったらまじで是非教えて欲しい・・・。
まだまだRxのお勉強は始まったばかり・・・
毎日テサグリング・・・
それからコードレシピの感想
かなり気楽に投稿できるもんだと勝手に思ってたorz
でもよく考えたらサンプルなんだしきちんと説明とか書かなきゃだ。
がんばろう、俺!
Observable.Generate、そして非同期実行
Reactive Extensionsのお勉強の続き。
今日はObservable.Generate メソッドに注目した。
というのも、
「あるメソッドの戻り値が一定の値になるまで、戻り値をそのメソッドの引数にして非同期で実行し続けるにはどうすればいいか?」
という問題にぶち当たっていたから。
非同期と言えばRxでしょう(当社比)から何かしらうまいことできるんじゃないのと思っていたけど勉強始めたばかりなので全然アイデアが浮かばなかった。
で、「非同期だからFromAsyncPattern?」とか数々の勘違いを積み重ねた上で「これ使えるんじゃね?」って思ったのがこのObservable.Generateメソッド。
ざっと紹介すると
using System; using System.Reactive.Linq; namespace ConsoleApplication19 { class Program { static void Main(string[] args) { Observable.Generate( 1, //初期値。 x => x < 10, //終了判定。falseになれば終了。 x => x * 2, //イテレーション。次の値を返す。 x => x.ToString("# です!")//セレクタ ).Subscribe( x => Console.WriteLine(x)); Console.WriteLine("*****終了*****"); Console.ReadKey(); } } }
のように、初期値と終了判定とイテレーションとセレクタを指定すればプッシュしてくれるもの。
これをさっきの
「あるメソッドの戻り値が一定の値になるまで、戻り値をそのメソッドの引数にして実行し続けるにはどうすればいいか?」
に当てはめれば
・・・うーん、なんかできそうな感じしてきた。
しかし!
一番肝心な非同期はどうすんのよ。。。
惜しい、すごく惜しい・・・。
・・・と思ったんだけど、公式サンプルにもあるとおり第5引数にScheduler.ThreadPoolを指定することで別スレッドで実行してくれるということに気づいた。
ヤッター、というか早く気づいとけ。
■Scheduler.ThreadPoolを指定した例
using System; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; namespace ConsoleApplication18 { class Program { static void Main(string[] args) { var o = Observable.Generate( 1, i => i < 5, i => AsyncFunc(i), i => i, Scheduler.ThreadPool //← これ! ); o.Subscribe( //OnNext i => Console.WriteLine("{0} called", i), //エラーハンドリング e => Console.WriteLine(e.Message), //完了時のコールバック () => Console.WriteLine("========すべての作業は終了しました。========") ); Console.WriteLine("*************メインスレッドの作業は終了しました***************"); Console.WriteLine("何かキーを押すと終了します。"); Console.ReadKey(); } static int AsyncFunc(int value) { Console.WriteLine("Start...."); //三秒待つ Thread.Sleep(3000); Console.WriteLine("{0} executed", value); //インクリメントする return ++value; } } }
■結果
*************メインスレッドの作業は終了しました***************" 何かキーを押すと終了します。 1 called Start.... 1 executed 2 called Start.... 2 executed 3 called Start.... 3 executed 4 called Start.... 4 executed ========すべての作業は終了しました。========
やったー!
非同期実行されてるぜ!!
ちなみに、Scheduler.ThreadPoolを指定して別スレッドで実行することを指示できるのは何もこのメソッドに限ったことでは無く、
Observableの拡張メソッドの多くはこの機能を備えている。