次の方法で共有


アプリのパフォーマンスを向上させる

アプリのパフォーマンスの低下は、さまざまな形で現れます。 アプリが応答しなくなったように見え、スクロールが遅くなり、デバイスのバッテリー寿命が短縮される可能性があります。 ただし、パフォーマンスの最適化には、単に効率的なコードを実装するだけではありません。 アプリのパフォーマンスに関するユーザーエクスペリエンスも考慮する必要があります。 たとえば、ユーザーが他のアクティビティを実行できないように操作を確実に実行すると、ユーザーのエクスペリエンスを向上させることができます。

.NET マルチプラットフォーム アプリ UI (.NET MAUI) アプリのパフォーマンスと認識されるパフォーマンスを向上させる手法は多数あります。 これらの手法をまとめて、CPU によって実行される作業量と、アプリによって消費されるメモリの量を大幅に削減できます。

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

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

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

アプリをプロファイリングする場合は、次のベスト プラクティスをお勧めします。

  • シミュレーターではアプリのパフォーマンスがゆがむ可能性があるため、シミュレーターでアプリをプロファイリングすることは避けてください。
  • 1 つのデバイスでパフォーマンス測定を行っても、他のデバイスのパフォーマンス特性が常に示されるとは限らないので、プロファイリングはさまざまなデバイスで実行することをお勧めします。 ただし、少なくとも、予想される仕様が最も低いデバイスでプロファイリングを実行する必要があります。
  • 他のすべてのアプリを閉じて、他のアプリではなく、プロファイリング対象のアプリの影響がすべて測定されていることを確認します。

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

コンパイルされたバインドでは、リフレクションを使用して実行時ではなく、コンパイル時にバインド式を解決することで、.NET MAUI アプリのデータ バインディングのパフォーマンスが向上します。 バインド式をコンパイルすると、通常、クラシック バインディングを使用するよりも 8 ~ 20 倍速くバインドを解決するコンパイル済みコードが生成されます。 詳細については、「コンパイル済みバインドの」を参照してください。

不要なバインドを減らす

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

正しいレイアウトを選択する

複数の子を表示できるが、1 人の子しか持たないレイアウトは無駄です。 たとえば、次の例は、1 人の子を持つ VerticalStackLayout を示しています。

<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 つの手法があります。 1 つ目は、表示されない要素を非表示にすることです。 各要素の IsVisible プロパティは、要素を画面上に表示するかどうかを決定します。 要素が他の要素の背後に隠れているために表示されない場合は、要素を削除するか、その IsVisible プロパティを falseに設定します。 要素の 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>

アプリ リソースの詳細については、「XAMLを使用したアプリのスタイル 」を参照してください。

アプリのサイズを小さくする

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

リンカーの動作を構成するための詳細については、「Android アプリをリンクする」、「iOS アプリをリンクする」、および「Mac Catalyst アプリをリンクする」を参照してください。

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

すべてのアプリには、アクティブ化期間があります。これは、アプリが起動されてからアプリを使用する準備ができるまでの時間です。 アプリの起動期間はユーザーにとってアプリの第一印象を与えるものであるため、アプリの好ましい第一印象を得るためには、この起動期間とそれに対するユーザーの認識を短縮することが重要です。

アプリは、最初の UI を表示する前に、アプリが起動していることをユーザーに示すスプラッシュ画面を提供する必要があります。 アプリで最初の UI をすばやく表示できない場合は、スプラッシュ画面を使用してアクティブ化期間の進行状況をユーザーに通知し、アプリがハングしていないことを安心させる必要があります。 この安心感は、進行状況バーまたは同様のコントロールである可能性があります。

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

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

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

別の方法として、ファクトリを使用して手動で実装することで、依存関係の挿入のパフォーマンスを高めることができます。

シェル アプリを作成する

.NET MAUI Shell アプリは、ポップアップとタブに基づいて、意見を持ったナビゲーション エクスペリエンスを提供します。 アプリのユーザー エクスペリエンスをシェルで実装できる場合は、これを行うと便利です。 シェル アプリは、TabbedPageを使用するアプリで発生するアプリの起動時ではなく、ナビゲーションに応答して必要に応じてページが作成されるため、起動エクスペリエンスの低下を回避するのに役立ちます。 詳細については、「シェルの概要」を参照してください。

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

ListViewを使用する場合は、最適化する必要があるユーザー エクスペリエンスがいくつかあります。

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

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

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

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

基礎

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

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

UI

UI コントロールで TAP を使用する場合は、次のガイドラインに従う必要があります。

  • API が使用可能な場合は、非同期バージョンの 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 への呼び出しをラップする。

IDisposable オブジェクトを using 文でラップする

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

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

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

IDisposable.Dispose の呼び出しを try/finally ブロックでラップする

次の例は、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 で強力な循環参照を回避する

場合によっては、オブジェクトがガベージ コレクターによってメモリを再利用できないようにする強力な参照サイクルを作成できます。 たとえば、次の例に示すように、UIViewから継承するクラスなどの NSObject-derived サブクラスが 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 インスタンスを作成します。これは、マネージド オブジェクトがそれに対する参照を保持する保証がないためです。 マネージド コードの観点からは、MyView オブジェクトは、AddSubview(UIView) 呼び出しが GCHandle用でない場合は再利用されます。

アンマネージド 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 でも発生します。 たとえば、Delegate プロパティまたは UITableView クラスの 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;
    }
}