パフォーマンスの最適化 : オブジェクトの動作
WPF オブジェクトに固有の動作を理解することは、機能とパフォーマンスのバランスを見極めるうえで役に立ちます。
このトピックは、次のセクションで構成されています。
- オブジェクトのイベント ハンドラーを削除しないとオブジェクトが維持される可能性がある
- 依存関係プロパティと依存関係オブジェクト
- Freezable オブジェクト
- ユーザー インターフェイスの仮想化
- 関連トピック
オブジェクトのイベント ハンドラーを削除しないとオブジェクトが維持される可能性がある
オブジェクトがそのイベントに渡すデリゲートは、事実上そのオブジェクトへの参照です。 このため、イベント ハンドラーによってオブジェクトが本来より長く維持される可能性があります。 オブジェクトのイベントをリッスンするように登録されているオブジェクトのクリーンアップを実行するときには、オブジェクトを解放する前にそのデリゲートを削除することが重要です。 不要なオブジェクトが残っていると、アプリケーションのメモリ使用量が増加します。 これは、オブジェクトが論理ツリーやビジュアル ツリーのルートである場合には特に重要になります。
WPF によって導入される弱いイベント リスナー パターンは、ソースとリスナーのオブジェクト有効期間の関係の追跡が困難な場合に役に立ちます。 一部の既存の WPF イベントはこのパターンを使用します。 カスタム イベントを持つオブジェクトを実装する場合に、このパターンが役に立つこともあります。 詳細については、「弱いイベント パターン」を参照してください。
CLR プロファイラーや作業セット ビューアーなど、特定のプロセスのメモリ使用量に関する情報を入手できるツールもいくつかあります。 CLR プロファイラーには、割り当てられた型のヒストグラム、割り当てグラフと呼び出しグラフ、さまざまなジェネレーションのガベージ コレクションとその結果のマネージ ヒープの状態を示す時系列、メソッドごとの割り当てとアセンブリの読み込みを示す呼び出しツリーなど、非常に便利な割り当てプロファイルのビューがいくつか含まれています。 詳細については、.NET Framework Developer Center を参照してください。
依存関係プロパティと依存関係オブジェクト
一般に、DependencyObject の依存関係プロパティへのアクセスが CLR プロパティへのアクセスに比べて遅いということはありません。 プロパティ値の設定には多少のパフォーマンス オーバーヘッドがありますが、値の取得は、CLR プロパティから取得する場合と同じくらい高速です。 この小さなパフォーマンス オーバーヘッドがある代わりに、依存関係プロパティでは、データ バインディング、アニメーション、継承、スタイル設定などの堅牢な機能がサポートされています。 詳細については、「依存関係プロパティの概要」を参照してください。
DependencyProperty の最適化
アプリケーションで依存関係プロパティを定義するときには注意が必要です。 定義する DependencyProperty が描画型のメタデータ オプションにしか影響しない場合 (AffectsMeasure などのその他のメタデータ オプションには影響しない場合) は、メタデータをオーバーライドしてそのようにマークする必要があります。 プロパティ メタデータをオーバーライドまたは取得する方法の詳細については、「依存関係プロパティのメタデータ」を参照してください。
すべてのプロパティ変更が実際に測定、配置、および描画に影響するわけではない場合は、測定、配置、および描画の各パスを、プロパティ変更ハンドラーによって手動で無効にした方が効率的な場合もあります。 たとえば、設定した制限値より値が大きい場合にのみ背景を再描画する場合は、 設定した制限値を値が超えていた場合にのみプロパティ変更ハンドラーで描画を無効にします。
DependencyProperty を継承可能にするとパフォーマンスへの負荷が発生する
既定では、登録した依存関係プロパティは継承不可になります。 ただし、プロパティはどれでも明示的に継承可能にすることができます。 この機能は便利ですが、プロパティを継承可能にすると、プロパティの無効化の時間が増加して、パフォーマンスに影響します。
RegisterClassHandler は慎重に使用する
RegisterClassHandler を呼び出すと、インスタンスの状態を保存することができます。ただし、このハンドラーはすべてのインスタンスで呼び出されるということを認識しておく必要があります。これはパフォーマンス上問題になる可能性があります。 RegisterClassHandler を使用するのは、アプリケーションでインスタンスの状態を保存する必要がある場合だけにしてください。
DependencyProperty の既定値は登録時に設定する
既定値を必要とする DependencyProperty を作成するときには、DependencyProperty の Register メソッドにパラメーターとして渡される既定のメタデータを使用してその値を設定します。 コンストラクターや要素の各インスタンスでプロパティ値を設定するのではなく、この方法を使用するようにしてください。
Register を使用して PropertyMetadata の値を設定する
DependencyProperty を作成する場合、PropertyMetadata を設定するには、Register メソッドを使用する方法と OverrideMetadata メソッドを使用する方法があります。 オブジェクトの静的コンストラクターで OverrideMetadata を呼び出すこともできますが、これは最適な解決方法とは言えず、パフォーマンスに影響します。 最適なパフォーマンスを得るためには、Register の呼び出しの際に PropertyMetadata を設定します。
Freezable オブジェクト
Freezable は、固定されていない状態と固定された状態の 2 つの状態を持つ特殊な型のオブジェクトです。 可能な場合は常にオブジェクトを固定するようにすると、アプリケーションのパフォーマンスが向上し、作業セットを縮小できます。 詳細については、「Freezable オブジェクトの概要」を参照してください。
各 Freezable には、オブジェクトが変更されるたびに発生する Changed イベントがあります。 ただし、変更通知はアプリケーションのパフォーマンスに影響を与えます。
たとえば、次の例では、各 Rectangle が同じ Brush オブジェクトを使用しています。
rectangle_1.Fill = myBrush
rectangle_2.Fill = myBrush
rectangle_3.Fill = myBrush
' ...
rectangle_10.Fill = myBrush
rectangle_1.Fill = myBrush;
rectangle_2.Fill = myBrush;
rectangle_3.Fill = myBrush;
// ...
rectangle_10.Fill = myBrush;
WPF には、SolidColorBrush オブジェクトの Changed イベントに対するイベント ハンドラーが既定で用意されており、それを使用して Rectangle オブジェクトの Fill プロパティを無効にすることができます。 この場合、SolidColorBrush では、Changed イベントを発生させる必要が生じるたびに、各 Rectangle のコールバック関数を呼び出す必要があります。このコールバック関数の呼び出しが積み重なると、パフォーマンスが大幅に低下します。 さらに、この時点でハンドラーを追加または削除すると、アプリケーションでリスト全体を検査しなければならなくなるため、パフォーマンスに大きく影響します。 アプリケーションのシナリオで SolidColorBrush が変更されることがない場合は、Changed イベント ハンドラーの維持に無駄な負荷を費やすことになります。
Freezable を固定すると、変更通知の維持にリソースを費やす必要がなくなるため、パフォーマンスが向上します。 次の表は、単純な SolidColorBrush について、IsFrozen プロパティを true に設定した場合とそうでない場合のサイズの比較を示しています。 ここでは、10 の Rectangle オブジェクトの Fill プロパティに 1 つのブラシを適用する場合を想定しています。
状態 |
サイズ |
---|---|
固定された SolidColorBrush |
212 バイト |
固定されていない SolidColorBrush |
972 バイト |
この概念の例を次のコード サンプルに示します。
Dim frozenBrush As Brush = New SolidColorBrush(Colors.Blue)
frozenBrush.Freeze()
Dim nonFrozenBrush As Brush = New SolidColorBrush(Colors.Blue)
For i As Integer = 0 To 9
' Create a Rectangle using a non-frozed Brush.
Dim rectangleNonFrozen As New Rectangle()
rectangleNonFrozen.Fill = nonFrozenBrush
' Create a Rectangle using a frozed Brush.
Dim rectangleFrozen As New Rectangle()
rectangleFrozen.Fill = frozenBrush
Next i
Brush frozenBrush = new SolidColorBrush(Colors.Blue);
frozenBrush.Freeze();
Brush nonFrozenBrush = new SolidColorBrush(Colors.Blue);
for (int i = 0; i < 10; i++)
{
// Create a Rectangle using a non-frozed Brush.
Rectangle rectangleNonFrozen = new Rectangle();
rectangleNonFrozen.Fill = nonFrozenBrush;
// Create a Rectangle using a frozed Brush.
Rectangle rectangleFrozen = new Rectangle();
rectangleFrozen.Fill = frozenBrush;
}
固定されていない Freezable の Changed ハンドラーによってオブジェクトが維持される可能性がある
Freezable オブジェクトの Changed イベントにオブジェクトが渡すデリゲートは、事実上そのオブジェクトへの参照です。 このため、Changed イベント ハンドラーによってオブジェクトが本来より長く維持される可能性があります。 Freezable オブジェクトの Changed イベントをリッスンするように登録されているオブジェクトのクリーンアップを実行するときには、オブジェクトを解放する前にそのデリゲートを削除することが重要です。
WPF では、Changed イベントが内部でもフックされます。 たとえば、Freezable を値として受け取るすべての依存関係プロパティは、自動的に Changed イベントをリッスンします。 この概念の例として、Brush を受け取る Fill プロパティを次に示します。
Dim myBrush As Brush = New SolidColorBrush(Colors.Red)
Dim myRectangle As New Rectangle()
myRectangle.Fill = myBrush
Brush myBrush = new SolidColorBrush(Colors.Red);
Rectangle myRectangle = new Rectangle();
myRectangle.Fill = myBrush;
myRectangle.Fill に myBrush を割り当てると、その Rectangle オブジェクトを指すデリゲートが SolidColorBrush オブジェクトの Changed イベントに追加されます。 このため、次のコードを使用しても、myRect は実際にはガベージ コレクションの対象にはなりません。
myRectangle = Nothing
myRectangle = null;
この場合、myBrush は myRectangle を引き続き維持し、それを Changed イベントを発生させるときにコールバックします。 myBrush を新しい Rectangle の Fill プロパティに割り当てても、myBrush にイベント ハンドラーがさらに追加されるだけです。
この種のオブジェクトをクリーンアップするための推奨される方法としては、Fill プロパティから Brush を削除します。これにより、Changed イベント ハンドラーも削除されます。
myRectangle.Fill = Nothing
myRectangle = Nothing
myRectangle.Fill = null;
myRectangle = null;
ユーザー インターフェイスの仮想化
WPF には、データ バインド子コンテンツを自動的に "仮想化" する StackPanel 要素のバリエーションも用意されています。 ここで、"仮想化" は、どの項目を画面に表示するかに基づいて、オブジェクトのサブセットを多数のデータ項目から生成する手法を指します。 特定の時間に画面に UI 要素が少ししか表示されていない場合に多数の UI 要素を生成すると、メモリおよびプロセッサの両方に負荷がかかります。 VirtualizingStackPanel は、VirtualizingPanel で提供される機能を通じて、表示される項目を計算し、ItemsControl (ListBox や ListView など) の ItemContainerGenerator と共に機能することで、表示される項目に対応する要素だけを作成します。
パフォーマンスの最適化の一環として、これらの項目のビジュアル オブジェクトは、画面に表示される場合にのみ生成または維持されます。 既にコントロールの表示可能領域にないビジュアル オブジェクトは削除される可能性があります。 これをデータの仮想化と混同しないようにしてください。データの仮想化では、すべてのデータ オブジェクトがローカル コレクションに存在するのではなく、データ オブジェクトが必要に応じてストリームされます。
次の表は、5000 の TextBlock 要素を StackPanel と VirtualizingStackPanel に追加して描画した場合の経過時間を示しています。 このシナリオの測定値は、テキスト文字列を ItemsControl オブジェクトの ItemsSource プロパティに割り当ててから、パネル要素にそのテキスト文字列が表示されるまでの時間を表します。
ホスト パネル |
レンダリング時間 (ミリ秒) |
---|---|
3210 |
|
46 |