パフォーマンスの最適化: オブジェクトの動作

WPF オブジェクトに固有の動作を理解することは、機能とパフォーマンスのバランスを見極めるうえで役に立ちます。

オブジェクトのイベント ハンドラーを削除しないとオブジェクトが維持される可能性がある

オブジェクトがそのイベントに渡すデリゲートは、事実上そのオブジェクトへの参照です。 このため、イベント ハンドラーによってオブジェクトが本来より長く維持される可能性があります。 オブジェクトのイベントをリッスンするように登録されているオブジェクトのクリーンアップを実行するときには、オブジェクトを解放する前にそのデリゲートを削除することが重要です。 不要なオブジェクトが残っていると、アプリケーションのメモリ使用量が増加します。 これは、オブジェクトが論理ツリーやビジュアル ツリーのルートである場合には特に重要になります。

WPF によって導入される弱いイベント リスナー パターンは、ソースとリスナーのオブジェクト有効期間の関係の追跡が困難な場合に役に立ちます。 一部の既存の WPF イベントはこのパターンを使用します。 カスタム イベントを持つオブジェクトを実装する場合に、このパターンが役に立つこともあります。 詳細については、「弱いイベント パターン」を参照してください。

CLR プロファイラーや作業セット ビューアーなど、特定のプロセスのメモリ使用量に関する情報を入手できるツールもいくつかあります。 CLR プロファイラーには、割り当てられた型のヒストグラム、割り当てグラフと呼び出しグラフ、さまざまなジェネレーションのガベージ コレクションとその結果のマネージド ヒープの状態を示す時系列、メソッドごとの割り当てとアセンブリの読み込みを示す呼び出しツリーなど、非常に便利な割り当てプロファイルのビューがいくつか含まれています。 詳しくは、「パフォーマンス」をご覧ください。

依存関係プロパティと依存関係オブジェクト

一般に、DependencyObject の依存関係プロパティへのアクセスが CLR プロパティへのアクセスに比べて遅いということはありません。 プロパティ値の設定には多少のパフォーマンス オーバーヘッドがありますが、値の取得は、CLR プロパティから取得する場合と同じくらい高速です。 この小さなパフォーマンス オーバーヘッドがある代わりに、依存関係プロパティでは、データ バインディング、アニメーション、継承、スタイル設定などの堅牢な機能がサポートされています。 依存関係プロパティの詳細については、「依存関係プロパティの概要」を参照してください。

DependencyProperty の最適化

アプリケーションで依存関係プロパティを定義するときには注意が必要です。 DependencyProperty がレンダリング型のメタデータ オプションにしか影響しない場合 (AffectsMeasure などの他のメタデータ オプションには影響しない場合) は、メタデータをオーバーライドしてそのようにマークする必要があります。 プロパティ メタデータをオーバーライドまたは取得する方法の詳細については、「依存関係プロパティのメタデータ」を参照してください。

すべてのプロパティ変更が実際に測定、配置、描画に影響するわけではない場合は、測定、配置、描画の各パスを、プロパティ変更ハンドラーによって手動で無効にした方が効率的な場合もあります。 たとえば、設定した制限値より値が大きい場合にのみ背景を再描画する場合は、 設定した制限値を値が超えていた場合にのみプロパティ変更ハンドラーで描画を無効にします。

DependencyProperty を継承可能にするとパフォーマンスへの負荷が発生する

既定では、登録した依存関係プロパティは継承不可になります。 ただし、プロパティはどれでも明示的に継承可能にすることができます。 この機能は便利ですが、プロパティを継承可能にすると、プロパティの無効化の時間が増加して、パフォーマンスに影響します。

RegisterClassHandler は慎重に使用する

RegisterClassHandler を呼び出している間に、インスタンスの状態を保存することができます。ただし、このハンドラーはすべてのインスタンスで呼び出されるということを認識しておく必要があります。これはパフォーマンス上問題になる可能性があります。 RegisterClassHandler を使用するのは、アプリケーションでインスタンスの状態を保存する必要がある場合だけにしてください。

DependencyProperty の既定値は登録時に設定する

既定値を必要とする DependencyProperty を作成するときは、DependencyPropertyRegister メソッドにパラメーターとして渡される既定のメタデータを使用して値を設定します。 コンストラクターや要素の各インスタンスでプロパティ値を設定するのではなく、この方法を使用するようにしてください。

Register を使用して PropertyMetadata の値を設定する

DependencyProperty を作成するときは、Register メソッドまたは OverrideMetadata メソッドのいずれかを使用して、PropertyMetadata を設定できます。 オブジェクトの静的コンストラクターで 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 プロパティを無効にすることができます。 この場合、SolidColorBrushChanged イベントを発生させる必要があるたびに、各 Rectangle のコールバック関数を呼び出す必要があります。このコールバック関数の呼び出しが積み重なると、パフォーマンスが大幅に低下します。 さらに、この時点でハンドラーを追加または削除すると、アプリケーションでリスト全体を検査しなければならなくなるため、パフォーマンスに大きく影響します。 アプリケーションのシナリオで SolidColorBrush を変更することがない場合は、Changed イベント ハンドラーの維持コストが無駄になります。

Freezable を固定すると、変更通知の維持にリソースを費やす必要がなくなるため、パフォーマンスが向上します。 次の表では、簡単な SolidColorBrush について、IsFrozen プロパティを true に設定した場合とそうでない場合のサイズの比較を示します。 ここでは、10 個の Rectangle オブジェクトの Fill プロパティに 1 つのブラシを適用する場合を想定しています。

状態 Size
固定された SolidColorBrush 212 バイト
固定されていない SolidColorBrush 972 バイト

この概念の例を次のコード サンプルに示します。

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;
}
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

固定されていない Freezable の Changed ハンドラーによってオブジェクトが維持される可能性がある

オブジェクトによって Freezable オブジェクトの Changed イベントに渡されデリゲートは、事実上そのオブジェクトへの参照です。 このため、Changed イベント ハンドラーによってオブジェクトが本来より長く維持される可能性があります。 Freezable オブジェクトの Changed イベントをリッスンするように登録されているオブジェクトのクリーンアップを実行するときは、オブジェクトを解放する前にそのデリゲートを削除することが重要です。

WPF では、Changed イベントが内部でもフックされます。 たとえば、Freezable を値として受け取るすべての依存関係プロパティでは、自動的に Changed イベントがリッスンされます。 Brush を受け取る Fill プロパティでは、この概念が示されています。

Brush myBrush = new SolidColorBrush(Colors.Red);
Rectangle myRectangle = new Rectangle();
myRectangle.Fill = myBrush;
Dim myBrush As Brush = New SolidColorBrush(Colors.Red)
Dim myRectangle As New Rectangle()
myRectangle.Fill = myBrush

myRectangle.FillmyBrush を割り当てると、その Rectangle オブジェクトを指すデリゲートが、SolidColorBrush オブジェクトの Changed イベントに追加されます。 このため、次のコードを使用しても、myRect は実際にはガベージ コレクションの対象にはなりません。

myRectangle = null;
myRectangle = Nothing

この場合、myBrush では myRectangle が引き続き維持されており、Changed イベントが発生すると、それにコールバックします。 新しい RectangleFill プロパティにmyBrush を割り当てると、myBrush に別のイベント ハンドラーが追加されるだけであることに注意してください。

この種のオブジェクトをクリーンアップするために推奨される方法は、Fill プロパティから Brush を削除することです。このようにすると、Changed イベント ハンドラーも削除されます。

myRectangle.Fill = null;
myRectangle = null;
myRectangle.Fill = Nothing
myRectangle = Nothing

ユーザー インターフェイスの仮想化

WPF では、データバインドされた子コンテンツを自動的に "仮想化する" StackPanel 要素のバリエーションも提供されています。 ここで、"仮想化" は、どの項目を画面に表示するかに基づいて、オブジェクトのサブセットを多数のデータ項目から生成する手法を指します。 特定の時間に画面に UI 要素が少ししか表示されていない場合に多数の UI 要素を生成すると、メモリおよびプロセッサの両方に負荷がかかります。 VirtualizingStackPanel を使うと (VirtualizingPanel で提供される機能を通じて) 表示できる項目が計算され、ItemsControl (ListBoxListView など) からの ItemContainerGenerator を使用して、表示できる項目に対する要素だけが作成されます。

パフォーマンスの最適化の一環として、これらの項目のビジュアル オブジェクトは、画面に表示される場合にのみ生成または維持されます。 既にコントロールの表示可能領域にないビジュアル オブジェクトは削除される可能性があります。 これをデータの仮想化と混同しないようにしてください。データの仮想化では、すべてのデータ オブジェクトがローカル コレクションに存在するのではなく、データ オブジェクトが必要に応じてストリームされます。

次の表では、5000 個の TextBlock 要素を StackPanelVirtualizingStackPanel に追加してレンダリングした場合の経過時間を示します。 このシナリオの測定値は、テキスト文字列を ItemsControl オブジェクトの ItemsSource プロパティに割り当ててから、パネル要素にそのテキスト文字列が表示されるまでの時間を表します。

ホスト パネル レンダリング時間 (ミリ秒)
StackPanel 3210
VirtualizingStackPanel 46

関連項目