イベントハンドラーのメモリリークを回避する理由と方法


154

StackOverflowでいくつかの質問と回答を読むこと+=で、C#(または他の.net言語)を使用してイベントハンドラーを追加すると、一般的なメモリリークが発生する可能性があることに気づきました...

私は過去にこのようなイベントハンドラーを何度も使用しましたが、それらがアプリケーションでメモリリークを引き起こしたり、引き起こしたりしていることに気付いていませんでした。

これはどのように機能しますか(つまり、実際にメモリリークが発生するのはなぜですか)?
この問題を解決するにはどうすればよいですか?-=同じイベントハンドラを使用して十分ですか?
このような状況に対処するための一般的な設計パターンまたはベストプラクティスはありますか?
例:UIでいくつかのイベントを発生させるために多くの異なるイベントハンドラーを使用して、多くの異なるスレッドを持つアプリケーションをどのように処理するべきですか?

すでに構築された大きなアプリケーションでこれを効率的に監視するための良い簡単な方法はありますか?

回答:


188

原因は簡単に説明できます。イベントハンドラーがサブスクライブされている間、イベントのパブリッシャーはイベントハンドラーデリゲートを介してサブスクライバーへの参照を保持します(デリゲートがインスタンスメソッドであると想定)。

パブリッシャーがサブスクライバーよりも長く存続する場合、サブスクライバーへの参照が他にない場合でも、サブスクライバーは存続します。

イコールハンドラーを使用してイベントをサブスクライブ解除すると、はい、ハンドラーが削除され、リークの可能性があります。ただし、私の経験では、これが実際に問題になることはめったにありません。通常、パブリッシャーとサブスクライバーのライフタイムはほぼ同じであるためです。

それ考えられる原因です...しかし、私の経験で、それかなり過剰に宣伝されています。もちろん、走行距離は異なる場合があります。注意が必要です。


...「。netで最も一般的なメモリリークは何か」などの質問への回答について、これについて書いている人がいるのを見てきました。
gillyb 2010

32
これをパブリッシャー側から回避する方法は、これ以上発生しないことが確認できたら、イベントをnullに設定することです。これにより、すべてのサブスクライバーが暗黙的に削除され、特定のイベントがオブジェクトの有効期間の特定の段階でのみ発生する場合に役立ちます。
JSBձոգչ2010

2
Diposeメソッドは、イベントをnullに設定する良い
機会です

6
@DaviFiamenghi:まあ、何かが処分されている場合、それは少なくともガベージコレクションの対象になることを示している可能性が高いです。
Jon Skeet、2015

1
@ BrainSlugs83:「とにかく一般的なイベントパターンには送信者が含まれます」-はい、しかしそれはイベントプロデューサーです。通常、イベントサブスクライバーインスタンスは関連し、送信者は関連しません。だから、はい、静的メソッドを使用してサブスクライブできる場合、これは問題ではありませんが、私の経験ではこれはめったに選択肢になりません。
Jon Skeet

13

はい、-=それで十分です。ただし、割り当てられたすべてのイベントを追跡することは非常に困難です。(詳細については、Jonの投稿を参照してください)。デザインパターンについては、弱いイベントパターンをご覧ください


1
msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx 4.0バージョンにはまだあります。
Femaref 2015年

パブリッシャーがサブスクライバーよりも長く存続することがわかっている場合は、サブスクライバーを作成し、IDisposableイベントのサブスクライブを解除します。
Shimmy Weitzhandler 2015年

9

この混乱については、https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16のブログで説明しています。ここにまとめて、わかりやすいようにします。

参照は、「必要」を意味します:

まず、オブジェクトAがオブジェクトBへの参照を保持している場合、オブジェクトAが機能するにはオブジェクトBが必要であることを理解する必要があります。したがって、ガベージコレクターは、オブジェクトAがメモリ内に存在している限り、オブジェクトBを収集しません。

この部分は開発者にとって明らかなはずです。

+ =右側のオブジェクトの参照を左側のオブジェクトに注入することを意味します。

しかし、混乱はC#+ =演算子に起因します。この演算子は、この演算子の右側が実際に左側のオブジェクトへの参照を注入していることを開発者に明確に伝えていません。

ここに画像の説明を入力してください

そうすることで、オブジェクトAはオブジェクトBを必要とすると考えますが、あなたの視点からは、オブジェクトAはオブジェクトBが生きているかどうかを気にする必要はありません。オブジェクトAはオブジェクトBが必要であると考えるので、オブジェクトAが生きている限り、オブジェクトAはオブジェクトBをガベージコレクタから保護します。しかし、イベントサブスクライバーオブジェクトに保護を与えたくない場合は、メモリリークが発生したと言えます。

ここに画像の説明を入力してください

このようなリークは、イベントハンドラを切り離すことで回避できます。

どのように決定するのですか?

ただし、コードベース全体には多数のイベントとイベントハンドラーがあります。それは、イベントハンドラーをどこでも切り離し続ける必要があることを意味しますか?答えは「いいえ」です。そうしなければならなかった場合、コードベースは冗長になって非常に醜くなります。

単純なフローチャートに従って、分離イベントハンドラーが必要かどうかを判断できます。

ここに画像の説明を入力してください

ほとんどの場合、イベントサブスクライバーオブジェクトはイベントパブリッシャーオブジェクトと同じくらい重要であり、両方が同時に存在することになっています。

心配する必要がないシナリオの例

たとえば、ウィンドウのボタンクリックイベント。

ここに画像の説明を入力してください

ここで、イベントパブリッシャーはButtonで、イベントサブスクライバーはMainWindowです。そのフローチャートを適用して質問すると、メインウィンドウ(イベントサブスクライバー)はボタン(イベントパブリッシャー)の前に無効になるはずですか?明らかに違いますよね?それでも意味がありません。では、なぜクリックイベントハンドラの分離を心配するのでしょうか。

イベントハンドラの切り離しが必須の場合の例。

サブスクライバーオブジェクトがパブリッシャーオブジェクトの前に無効になるはずの1つの例を示します。たとえば、MainWindowが「SomethingHappened」という名前のイベントを発行し、ボタンクリックでメインウィンドウから子ウィンドウを表示するとします。子ウィンドウは、メインウィンドウのそのイベントをサブスクライブします。

ここに画像の説明を入力してください

また、子ウィンドウはメインウィンドウのイベントをサブスクライブします。

ここに画像の説明を入力してください

このコードから、メインウィンドウにボタンがあることを明確に理解できます。そのボタンをクリックすると、子ウィンドウが表示されます。子ウィンドウはメインウィンドウからイベントをリッスンします。何かをした後、ユーザーは子ウィンドウを閉じます。

ここで、私が提供したフローチャートに従って、「子ウィンドウ(イベントサブスクライバー)はイベントパブリッシャー(メインウィンドウ)の前にデッドになるはずですか?答えはYESであるはずです。そうです。それで、イベントハンドラーを切り離します。 。私は通常、ウィンドウのUnloadedイベントからそれを行います。

経験則:ビュー(WPF、WinForm、UWP、Xamarinフォームなど)がViewModelのイベントをサブスクライブする場合は、必ずイベントハンドラーをデタッチすることを忘れないでください。ViewModelは通常、ビューよりも長持ちするためです。そのため、ViewModelが破棄されない場合、そのViewModelのサブスクライブイベントを含むビューはメモリ内にとどまるので、好ましくありません。

メモリプロファイラーを使用した概念の証明。

メモリプロファイラーでコンセプトを検証できない場合は、それほど楽しくありません。この実験では、JetBrain dotMemoryプロファイラーを使用しました。

まず、次のように表示されるMainWindowを実行しました。

ここに画像の説明を入力してください

次に、メモリのスナップショットを撮りました。次に、ボタンを3回クリックしました。3つの子ウィンドウが表示されました。これらの子ウィンドウをすべて閉じ、dotMemoryプロファイラーのForce GCボタンをクリックして、ガベージコレクターが確実に呼び出されるようにしました。次に、別のメモリスナップショットを撮って比較しました。見よ!私たちの恐れは本当でした。チャイルドウィンドウは、閉じてもガベージコレクターによって収集されませんでした。それだけでなく、ChildWindowオブジェクトのリークされたオブジェクト数も「3」と表示されます(ボタンを3回クリックして3つの子ウィンドウを表示しました)。

ここに画像の説明を入力してください

では、次のようにイベントハンドラーを切り離しました。

ここに画像の説明を入力してください

次に、同じ手順を実行し、メモリプロファイラーを確認しました。今回はすごい!これ以上のメモリリークはありません。

ここに画像の説明を入力してください


3

イベントは実際にはイベントハンドラーのリンクされたリストです

イベントで+ = new EventHandlerを実行する場合、この特定の関数が以前にリスナーとして追加されているかどうかは問題ではなく、+ =ごとに1回追加されます。

イベントが発生すると、リンクリストを項目ごとに通過し、このリストに追加されたすべてのメソッド(イベントハンドラー)を呼び出します。これが、ページが実行されていない限り、イベントハンドラーが呼び出され続ける理由です。彼らは生きている(根付く)、そしてそれらが接続されている限り彼らは生きているでしょう。したがって、イベントハンドラーが-= new EventHandlerでフック解除されるまで、それらは呼び出されます。

こちらをご覧ください

そしてMSDNはこちら


また、を参照してください。blogs.msdn.com/b/tess/archive/2006/01/23/...
コーディグレー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.