アプリのパフォーマンス向上

アプリのパフォーマンス低下は、さまざまな挙動で現れます。 たとえば、アプリが応答しない、スクロールが遅くなる、デバイス電池の寿命が縮まるなどです。 ただし、パフォーマンスを最適化するには、効率的なコードを実装するだけでは済みません。 アプリのパフォーマンスに関わるユーザー エクスペリエンスも考慮する必要があります。 たとえば、操作の実行によって、ユーザーが他の操作を実行できない状況にならないようにすることで、ユーザー エクスペリエンスを改善できます。

.NET Multi-platform App UI (.NET MAUI) アプリのパフォーマンスと認識されるパフォーマンスを向上させる手法は多数あります。 これらの手法をすべて使用することで、CPU で実行される作業量や、アプリで消費されるメモリ量を大幅に減らすことができます。

プロファイラーを使用する

アプリを開発する場合、プロファイリング後にコードの最適化のみを試みることが重要です。 プロファイリングは、パフォーマンスの問題を減らすために、コードの最適化で最も大きな効果がある場所を特定するための手法です。 プロファイラーは、アプリケーションのメモリ使用量を追跡し、アプリのメソッドの実行時間を記録します。 このデータは、最適化の絶好の機会を見つけられるように、アプリの実行パスと、コードの実行コストをナビゲートする場合に役立ちます。

.NET MAUI アプリは、Android、iOS、Mac、Windows、Windows の PerfView で dotnet-trace を使用してプロファイリングできます。 詳細については、「.NET MAUI アプリのプロファイリング」をご覧ください。

次のベスト プラクティスは、アプリのプロファイリングを行う場合に推奨されます。

  • シミュレーターはアプリのパフォーマンスを損なう可能性があるため、シミュレーターではアプリのプロファイリングは行わないでください。
  • 1 つのデバイスでパフォーマンスを測定する際に、必ずしも他のデバイスのパフォーマンス特性が示されるわけではないため、さまざまなデバイスでプロファイリングを実行するのが理想的です。 ただし、少なくとも、性能が最も低いと思われるデバイスでプロファイリングを行う必要があります。
  • 他のすべてのアプリを終了し、他のアプリではなく、プロファイリングを行うアプリのあらゆる影響を測定するようにしてください。

コンパイル済みのバインドを使用する

コンパイル済みのバインドでは、リフレクションでのランタイムではなく、コンパイル時にバインド式を解決することで、.NET MAUI アプリでのデータ バインディングのパフォーマンスが向上します。 バインド式をコンパイルすると、通常、従来のバインドの 8 から 20 倍の速さでバインドを解決するコンパイル済みのコードが生成されます。 詳しくは、「コンパイル済みのバインド」をご覧ください。

不要なバインドを減らす

簡単に静的に設定できるコンテンツにはバインドを利用しないでください。 バインドする必要のないデータをバインドすることには何の利点もありません。バインドはコスト効果が高くありません。 たとえば、Button.Text = "Accept" を設定すると、値が "Accept" のビューモデル プロパティ stringButton.Text をバインドするより、オーバーヘッドが少なくなります。

適切なレイアウトを選択する

複数の子を表示できるが、子が 1 つしかないレイアウトは不経済です。 たとえば、次のコード例では、VerticalStackLayout と 1 つの子を確認できます。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <Image Source="waterfront.jpg" />
    </VerticalStackLayout>
</ContentPage>

これは不要なため、VerticalStackLayout 要素は次の例のように削除してください。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Image Source="waterfront.jpg" />
</ContentPage>

また、他のレイアウトを組み合わせて特定のレイアウトの外観を再現することはお控えください。不要なレイアウト計算が実行されます。 たとえば、HorizontalStackLayout 要素を組み合わせて、Grid レイアウトを再現しないでください。 次の例は、不適切な例を示しています。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Name:" />
            <Entry Placeholder="Enter your name" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Age:" />
            <Entry Placeholder="Enter your age" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Occupation:" />
            <Entry Placeholder="Enter your occupation" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Address:" />
            <Entry Placeholder="Enter your address" />
        </HorizontalStackLayout>
    </VerticalStackLayout>
</ContentPage>

不要なレイアウト計算が行われるため、不経済です。 代わりに、次の例のように、Grid を使用すれば望ましいレイアウトを実現できます。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Grid ColumnDefinitions="100,*"
          RowDefinitions="30,30,30,30">
        <Label Text="Name:" />
        <Entry Grid.Column="1"
               Placeholder="Enter your name" />
        <Label Grid.Row="1"
               Text="Age:" />
        <Entry Grid.Row="1"
               Grid.Column="1"
               Placeholder="Enter your age" />
        <Label Grid.Row="2"
               Text="Occupation:" />
        <Entry Grid.Row="2"
               Grid.Column="1"
               Placeholder="Enter your occupation" />
        <Label Grid.Row="3"
               Text="Address:" />
        <Entry Grid.Row="3"
               Grid.Column="1"
               Placeholder="Enter your address" />
    </Grid>
</ContentPage>

イメージ リソースを最適化する

アプリが使用するリソースのうち最もコストが高いものとして画像があります。多くの場合、画像は高解像度でキャプチャされます。 画像は細かい部分まで鮮明になりますが、そのような画像を表示するアプリでは通常、画像をデコードするためにより多くの CPU を使用する必要があり、また、デコードされた画像を格納するためにより多くのメモリが必要になります。 表示サイズを小さくするためにスケールダウンする場合、メモリ内の高解像度画像をデコードするのは無駄です。 代わりに、予測された表示サイズに近い、格納された画像の多重解像度バージョンを作成して、CPU 使用量とメモリの占有領域を減らします。 たとえば、リスト ビューに表示される画像は、全画面で表示される画像よりも解像度が低くなる可能性が最も高くなります。

そのため、必要な場合にのみ作成し、アプリで不要になった場合はすぐに解放する必要があります。 たとえば、アプリがストリームからデータを読み込み、画像を表示する場合、必要なときだけストリームが作成されるようにします。また、不要になったら、ストリームを解放するようにします。 これは、ページが作成されたときや Page.Appearing イベントが発生したときにストリームを作成し、Page.Disappearing イベントが発生したときにストリームを解放することで達成されます。

ImageSource.FromUri(Uri) メソッドを使用して表示する画像をダウンロードする場合は、適切な期間、ダウンロードした画像キャッシュされることを確認します。 詳細については、「画像のキャッシュ」をご覧ください。

ビジュアル ツリーのサイズを減らす

ページ上の要素の数を減らすと、ページのレンダリングが速くなります。 これは主に 2 つの手法で達成できます。 最初の手法は、表示されない要素を隠すことです。 各要素の IsVisible プロパティは、要素をビジュアル ツリーに含めるかどうかを決定します。 そのため、ある要素が他の要素の後ろに隠れているために見えない場合、その要素を取り除くか、その IsVisible プロパティを false に設定します。

2 番目の手法は、不要な要素を取り除くことです。 たとえば、次の例では、複数の Label 要素が含まれるページ レイアウトを確認できます。

<VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Hello" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Welcome to the App!" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Downloading Data..." />
    </VerticalStackLayout>
</VerticalStackLayout>

要素の数を減らして同じページ レイアウトを維持できます。次の例をご覧ください。

<VerticalStackLayout Padding="20,35,20,20"
                     Spacing="25">
    <Label Text="Hello" />
    <Label Text="Welcome to the App!" />
    <Label Text="Downloading Data..." />
</VerticalStackLayout>

アプリケーション リソース ディクショナリのサイズを減らす

アプリ全体で使用されるリソースは、重複を回避するために、アプリのリソース ディクショナリに保存してください。 アプリ全体で解析しなければならない XAML の量を減らすことができます。 次の例で HeadingLabelStyle リソースを確認できます。このリソースはアプリ全体で使用されるため、アプリのリソース ディクショナリで定義されています。

<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.App">
     <Application.Resources>
        <Style x:Key="HeadingLabelStyle"
               TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
     </Application.Resources>
</Application>

ただし、あるページに固有の XAML は、アプリのリソース ディクショナリには含める必要はありません。アプリのリソースは、ページが必要とするときではなく、アプリの起動時に解析されるためです。 リソースが起動ページではないページで使用される場合、そのページのリソース ディクショナリに置いてください。これにより、アプリの起動時に解析される XAML が減ります。 次の例では、HeadingLabelStyle リソースを確認できます。このリソースは 1 つのページだけに存在するため、ページのリソース ディクショナリで定義されています。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <ContentPage.Resources>
        <Style x:Key="HeadingLabelStyle"
                TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
    </ContentPage.Resources>
    ...
</ContentPage>

アプリケーション リソースの詳細については、「Style apps using XAML」をご覧ください。

アプリケーションのサイズを縮小する

.NET MAUI によってアプリがビルドされるときに、ILLink というリンカーを使用してアプリの全体的なサイズを小さくできます。 ILLink は、コンパイラによって生成された中間コードを分析することで、サイズを小さくします。 使用されていないメソッド、プロパティ、フィールド、イベント、構造体、クラスを削除して、アプリの実行に必要なコードとアセンブリの依存関係のみを含むアプリを生成します。

リンカー動作の構成の詳細については、「Linking an Android app」、「Linking an iOS app」、「Linking a Mac Catalyst app」をご覧ください。

アプリのアクティブ化期間を短縮する

すべてのアプリにアクティブ化期間があります。これは、アプリが開始されてから、使用できるようになるまでの期間です。 このアクティブ化期間に、ユーザーにアプリの第一印象を与えることになります。したがって、ユーザーにアプリに対する好意的な第一印象を与えるためには、アクティブ化期間を減らし、ユーザーの認識を和らげることが重要です。

アプリで初期 UI が表示される前に、スプラッシュ スクリーンを提供し、アプリが起動中であることをユーザーに示す必要があります。 アプリで初期 UI をすぐに表示できない場合は、アプリがハングしていないことを確認させるために、スプラッシュ スクリーンを使用して、アクティブ化期間での進行状況をユーザーに知らせる必要があります。 この確認は進行状況バー、または同様のコントロールで行うことができます。

アクティブ化期間中に、アプリはアクティブ化ロジックを実行します。これには、多くの場合、リソースの読み込みと処理が含まれます。 リモートで取得されるのではなく、アプリ内に必要なリソースがパッケージ化されるようにすることで、アクティブ化期間を減らすことができます。 たとえば、ある状況では、アクティブ化期間中にローカルに格納されたプレースホルダー データを読み込むことが適切な場合があります。 その後、初期 UI が表示された時点で、ユーザーはアプリと対話でき、プレースホルダー データをリモート ソースから段階的に置き換えることができます。 さらに、アプリのアクティブ化ロジックでは、ユーザーがアプリの使用を開始するために必要な作業のみを実行する必要があります。 アセンブリは初回使用時に読み込まれるため、これは追加のアセンブリの読み込みを遅らせる場合に役立ちます。

依存関係挿入コンテナーを慎重に選択する

依存性の注入コンテナーにより、モバイル アプリに追加のパフォーマンス制約が導入されます。 コンテナーで型を登録し、解決すると、パフォーマンス コストが発生します。特に、アプリのページ ナビゲーションごとに依存関係が再構築される場合には、各型を作成するために、コンテナーでリフレクションが使用されるからです。 依存関係が多いか深い場合、作成コストが大幅に増加する可能性があります。 また、通常、アプリの起動中に発生する型の登録は、使用されるコンテナーによっては、起動時間に顕著な影響を与える可能性があります。 .NET MAUI アプリでの依存関係の挿入の詳細については、「依存関係の挿入」を参照してください。

代替手段としては、ファクトリを使用して手動実装することで、依存関係挿入の性能を上げることができます。

シェル アプリを作成する

.NET MAUI シェル アプリでは、ポップアップとタブに基づいて、厳格なナビゲーション エクスペリエンスを提供します。 アプリのユーザー エクスペリエンスをシェルで実装できる場合、そうすることが推奨されます。 シェル アプリは、不適切な初期設定を避ける目的で役立ちます。TabbedPage を使用するアプリで発生するようにアプリの起動時ではなく、ナビゲーションに対する応答で必要に応じてページが作成されるためです。 詳細については、「シェルの概要」をご覧ください。

ListView パフォーマンスを最適化する

ListView を使用する場合、最適化が必要なさまざまなユーザー エクスペリエンスがあります。

  • 初期化 – コントロールが作成されたときに始まり、項目が画面に表示されたときに終わる時間間隔。
  • スクロール – 一覧をスクロール表示し、UI がタッチ ジェスチャに遅れないようにする機能。
  • 項目の追加、削除、選択の相互作用

ListView コントロールは、アプリがデータとセルのテンプレートを提供することを必要とします。 その提供方法は、コントロールのパフォーマンスを大きな影響を与えます。 詳細については、「キャッシュ データ」を参照してください。

非同期プログラミングを使用する

非同期プログラミングを使用することにより、アプリ全体の応答性を向上させ、多くの場合はパフォーマンスのボトルネックを回避できます。 .NET では、非同期操作に推奨されるデザイン パターンは、タスクベースの非同期パターン (TAP) です。 ただし、TAP を不適切に使用すると、アプリのパフォーマンスが低下する可能性があります。

基礎

TAP を使用するときは、次の一般的なガイドラインに従う必要があります。

  • TaskStatus 列挙型によって表されるタスクのライフサイクルを理解します。 詳しくは、「TaskStatus の意味」および「タスクの状態」をご覧ください。
  • 一連の非同期操作を await で個別に待機するのではなく、Task.WhenAll メソッドを使用して、複数の非同期操作が完了するのを非同期に待機します。 詳しくは、「Task.WhenAll」をご覧ください。
  • 複数の非同期操作の 1 つが完了するのを非同期に待つには、Task.WhenAny メソッドを使用します。 詳しくは、「Task.WhenAny」をご覧ください。
  • 指定した時間の後で終了する Task オブジェクトを生成するには、Task.Delay メソッドを使用します。 これは、データをポーリングする場合や、ユーザー入力の処理を事前に定義された時間だけ遅延する場合などのシナリオに役立ちます。 詳しくは、「Task.Delay」をご覧ください。
  • スレッド プールで大量の同期 CPU 操作を実行するには、Task.Run メソッドを使用します。 このメソッドは TaskFactory.StartNew メソッドのショートカットであり、最適な引数が設定されています。 詳しくは、「Task.Run」をご覧ください。
  • 非同期コンストラクターを作成しないようにします。 すべての初期化を正しく await するには、代わりに、ライフサイクル イベントまたは個別の初期化ロジックを使用します。 詳しくは、blog.stephencleary.com の「非同期コンストラクター」をご覧ください。
  • アプリの起動時に非同期操作が完了するのを待たないようにするには、レイジー タスク パターンを使用します。 詳しくは、「AsyncLazy」をご覧ください。
  • TAP が使用されていない既存の非同期操作に対しては、TaskCompletionSource<T> オブジェクトを作成することによってタスク ラッパーを作成します。 これらのオブジェクトにより Task の利点がプログラミングで得られ、関連付けられた Task の有効期間と完了を制御できるようになります。 詳しくは、「TaskCompletionSource の性質」をご覧ください。
  • 非同期操作の結果を処理する必要がない場合は、待機された Task オブジェクトを返す代わりに、Task オブジェクトを返します。 これは、実行されるコンテキストの切り替えが少ないため、パフォーマンスが向上します。
  • 使用可能になった時点でデータを処理する場合や、相互に非同期的に通信する必要がある複数の操作がある場合は、タスク並列ライブラリ (TPL) データフロー ライブラリを使用します。 詳しくは、「データフロー (タスク並列ライブラリ)」をご覧ください。

UI

UI コントロールを備えた TAP を使用するときは、次のガイドラインに従う必要があります。

  • 使用可能な場合は、API の非同期バージョンを呼び出します。 これにより、UI スレッドの非ブロック状態が保たれ、アプリのユーザー エクスペリエンスの向上に役立ちます。

  • 例外がスローされるのを防ぐために、UI スレッドでの非同期操作からのデータを使用して UI 要素を更新します。 ただし、ListView.ItemsSource プロパティに対する更新は、UI スレッドに自動的にマーシャリングされます。 コードが UI スレッドで実行されているかどうかを判断する方法の詳細については、「UI スレッドでスレッドを作成する」をご覧ください。

    重要

    データ バインディングによって更新されるコントロール プロパティは、UI スレッドに自動的にマーシャリングされます。

エラー処理

TAP を使用するときは、次のエラー処理ガイドラインに従う必要があります。

  • 非同期例外処理について学習します。 非同期に実行されているコードによってスローされたハンドルされない例外は、特定の状況を除き、呼び出し元のスレッドに反映されます。 詳しくは、「例外処理 (タスク並列ライブラリ)」をご覧ください。
  • async void メソッドを作成せず、代わりに async Task メソッドを作成します。 これにより、エラー処理、構成可能性、およびテストの容易性が向上します。 このガイドラインに対する例外は非同期イベント ハンドラーであり、その場合は void を返す必要があります。 詳しくは、「async void を避ける」をご覧ください。
  • Task.WaitTask.Result、または GetAwaiter().GetResult メソッドを呼び出すことによって、ブロックと非同期コードを混在させないでください。デッドロックが発生する可能性があります。 ただし、このガイドラインに違反する必要がある場合は、GetAwaiter().GetResult メソッドを呼び出すことをお勧めします。この方法では、タスクの例外が保持されます。 詳しくは、「すべて非同期にする」および「.NET 4.5 でのタスク例外処理」をご覧ください。
  • 可能な限り ConfigureAwait メソッドを使用して、コンテキスト フリーのコードを作成します。 コンテキストフリーのコードは、モバイル アプリでのパフォーマンスが優れており、部分的に非同期のコードベースを使用するときのデッドロックを回避するのに役立つ手法です。 詳しくは、「コンテキストを構成する」をご覧ください。
  • 前の非同期操作によってスローされた例外の処理や、開始前または実行中の継続のキャンセルなどの機能には、"継続タスク" を使用します。 詳しくは、「継続タスクを使用したタスクの連結」をご覧ください。
  • ICommand から非同期操作を呼び出すときは、ICommand の非同期の実装を使用します。 これにより、非同期コマンド ロジック内のすべての例外を処理できます。 詳細については、「非同期プログラミング: 非同期 MVVM アプリケーションのパターン: コマンド」をご覧ください。

オブジェクトの作成コストの発生を遅らせる

遅延初期化を使用して、最初に使用されるまでオブジェクトの作成を遅らせることができます。 この手法は主に、パフォーマンスの改善、計算の回避、メモリ要件の縮小を目的として利用されます。

次のようなシナリオでは、作成コストのかかるオブジェクトに対して遅延初期化を使用することを検討してください。

  • アプリでオブジェクトを使用しない可能性がある。
  • オブジェクトが作成される前に、コストのかかる他の操作を完了する必要がある。

次の例に示すように、遅延初期化の型を定義する場合は、Lazy<T> クラスを使用します。

void ProcessData(bool dataRequired = false)
{
    Lazy<double> data = new Lazy<double>(() =>
    {
        return ParallelEnumerable.Range(0, 1000)
                     .Select(d => Compute(d))
                     .Aggregate((x, y) => x + y);
    });

    if (dataRequired)
    {
        if (data.Value > 90)
        {
            ...
        }
    }
}

double Compute(double x)
{
    ...
}

遅延初期化は、Lazy<T>.Value プロパティへの初回のアクセス時に発生します。 初回アクセス時にラップされた型が作成され、返されて、今後のアクセスのために保存されます。

遅延初期化の詳細については、「遅延初期化」を参照してください。

IDisposable のリソースを解放する

IDisposable インターフェイスは、リソースを解放するためのメカニズムを提供します。 リソースを明示的に解放するために実装する必要がある Dispose メソッドを提供します。 IDisposable はデストラクターではありません。以下の状況でのみ実装する必要があります。

  • クラスがアンマネージ リソースを所有している場合。 解放が必要な代表的なアンマネージ リソースには、ファイル、ストリーム、およびネットワーク接続が含まれます。
  • クラスがマネージ IDisposable リソースを所有している場合。

使用者は、インスタンスが必要でなくなったときに、 IDisposable.Dispose 実装を呼び出してリソースを解放できます。 これには、次の 2 つのアプローチがあります。

  • using ステートメントで IDisposable オブジェクトをラップする。
  • try/finally ブロックで IDisposable.Dispose 呼び出しをラップする。

using ステートメントで IDisposable オブジェクトをラップする

次の例は、using ステートメントでの IDisposable オブジェクトのラップ方法を示します。

public void ReadText(string filename)
{
    string text;
    using (StreamReader reader = new StreamReader(filename))
    {
        text = reader.ReadToEnd();
    }
    ...
}

StreamReader クラスは IDisposable を実装し、using ステートメントは、スコープを外れる前に StreamReader オブジェクトで StreamReader.Dispose メソッドを呼び出す便利な構文を提供します。 StreamReader オブジェクトは、using ブロック内では読み取り専用です。再割り当てすることはできません。 また、コンパイラは try/finally ブロックの中間言語 (IL) を実装するため、例外が発生した場合でも、using ステートメントで確実に Dispose メソッドを呼び出すことができます。

Try/Finally ブロックで IDisposable.Dispose 呼び出しをラップする

次の例は、try/finally ブロックで IDisposable.Dispose への呼び出しのラップ方法を示します。

public void ReadText(string filename)
{
    string text;
    StreamReader reader = null;
    try
    {
        reader = new StreamReader(filename);
        text = reader.ReadToEnd();
    }
    finally
    {
        if (reader != null)
            reader.Dispose();
    }
    ...
}

StreamReader クラスは IDisposable を実装し、finally ブロックはリソースを解放するために StreamReader.Dispose メソッドを呼び出します。 詳細については、IDisposable インターフェイスに関するページを参照してください。

イベントのサブスクライブ解除

メモリ リークを防ぐため、サブスクライバー オブジェクトが破棄される前に、イベントのサブスクリプションを解除する必要があります。 イベントのサブスクリプションを解除するまで、パブリッシュ側オブジェクトでイベントのデリゲートは、サブスクライバーのイベント ハンドラーをカプセル化するデリゲートへの参照を保持しています。 パブリッシュ側オブジェクトがこの参照を保持している限り、ガベージ コレクションはサブスクライバー オブジェクトのメモリを再利用しません。

次の例は、イベントからのサブスクリプションの解除方法を示します。

public class Publisher
{
    public event EventHandler MyEvent;

    public void OnMyEventFires()
    {
        if (MyEvent != null)
            MyEvent(this, EventArgs.Empty);
    }
}

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _publisher.MyEvent += OnMyEventFires;
    }

    void OnMyEventFires(object sender, EventArgs e)
    {
        Debug.WriteLine("The publisher notified the subscriber of an event");
    }

    public void Dispose()
    {
        _publisher.MyEvent -= OnMyEventFires;
    }
}

Subscriber クラスは、自身の Dispose メソッドでイベントのサブスクリプションを解除します。

また、ラムダ式ではオブジェクトを参照して保持できるため、イベント ハンドラーとラムダ構文を使用する場合、参照サイクルが発生することがあります。 したがって、次の例のように、匿名メソッドへの参照をフィールドに格納し、イベントのサブスクリプションの解除で使用できます。

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;
    EventHandler _handler;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _handler = (sender, e) =>
        {
            Debug.WriteLine("The publisher notified the subscriber of an event");
        };
        _publisher.MyEvent += _handler;
    }

    public void Dispose()
    {
        _publisher.MyEvent -= _handler;
    }
}

_handler フィールドは匿名メソッドへの参照を保持し、イベントのサブスクリプションとサブスクリプション解除に使用されます。

iOS や Mac Catalyst で強力な循環参照を回避する

場合によっては、ガベージ コレクターからオブジェクトのメモリが再要求されないように、強い参照循環を作成することがあります。 たとえば、NSObject から派生したサブクラス (UIView から継承されたクラスなど) は、次の例で示すように、NSObject から派生したコンテナーに追加され、Objective-C から強く参照されます。

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    Container _parent;

    public MyView(Container parent)
    {
        _parent = parent;
    }

    void PokeParent()
    {
        _parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView(container));

このコードで Container インスタンスを作成すると、C# オブジェクトは Objective-C オブジェクトに対して強い参照を持つことになります。 同様に、MyView インスタンスも、Objective-C オブジェクトに対して強い参照を持つことになります。

さらに、container.AddSubview を呼び出すと、アンマネージ インスタンス MyView の参照カウントが増えます。 この場合、.NET for iOS ランタイムは、マネージド コード内の MyView オブジェクトを有効な状態に維持する GCHandle インスタンスを作成します。これは、マネージド オブジェクトがそのオブジェクトへの参照を保持する保証がないためです。 マネージド コードの観点からは、GCHandle に対する AddSubview(UIView) の呼び出しではなくなった後に MyView オブジェクトは再要求されます。

アンマネージド MyView オブジェクトは、マネージド オブジェクトを示す GCHandle を持つことになります。これは強いリンクと呼ばれます。 マネージド オブジェクトには、Container インスタンスへの参照が含まれます。 そして Container インスタンスには MyView オブジェクトへのマネージド参照が含まれます。

含まれるオブジェクトがコンテナーに対するリンクを維持する場合、循環参照の処理に使用できる選択肢がいくつかあります。

  • コンテナーに対して弱い参照を維持することで、循環参照を避ける。
  • オブジェクトに対して Dispose を呼び出す。
  • コンテナーへのリンクを null に設定することで、手動で循環を中断する。
  • コンテナーに含まれているオブジェクトを手動で削除する。

弱い参照を使用する

循環を避ける方法の 1 つは、子から親への参照に弱い参照を使用することです。 たとえば、上記のコードは、次の例に示すようになります。

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    WeakReference<Container> _weakParent;

    public MyView(Container parent)
    {
        _weakParent = new WeakReference<Container>(parent);
    }

    void PokeParent()
    {
        if (weakParent.TryGetTarget (out var parent))
            parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView container));

ここで、含まれるオブジェクトでは、親が維持されません。 ただし、親では container.AddSubView を呼び出すことで子が維持されます。

これはデリゲートまたはデータ ソース パターンを使用する iOS API でも起こります。その場合、ピア クラスは実装を含みます。 たとえば、UITableView クラス内で Delegate プロパティまたは DataSource を設定する場合です。

プロトコルを実装するためだけに作成されたクラスの場合 (IUITableViewDataSource など) にできることは、サブクラスの作成ではなく、クラスにインターフェイスを実装し、メソッドをオーバーライドし、DataSource プロパティを this に割り当てることです。

強い参照を使用したオブジェクトを破棄する

強い参照が存在し、依存関係を削除するのが困難な場合は、Dispose メソッドで親ポインターをクリアします。

コンテナーの場合は、次の例に示すように、Dispose メソッドをオーバーライドして含まれるオブジェクトを削除します。

class MyContainer : UIView
{
    public override void Dispose()
    {
        // Brute force, remove everything
        foreach (var view in Subviews)
        {
              view.RemoveFromSuperview();
        }
        base.Dispose();
    }
}

親に対する強い参照を維持する子オブジェクトの場合は、Dispose 実装で親に対する参照をクリアします。

class MyChild : UIView
{
    MyContainer _container;

    public MyChild(MyContainer container)
    {
        _container = container;
    }

    public override void Dispose()
    {
        _container = null;
    }
}