Xamarin.Mac のしくみ
ほとんどの場合、開発者は Xamarin.Mac の内部 "マジック" について気にする必要はありません。ただし、内部での動作を大まかに理解しておくと、既存のドキュメントを C# レンズを使用して解釈したり、問題が発生したときにデバッグしたりする際に役立ちます。
Xamarin.Mac では、アプリケーションは、ネイティブ クラス (NSString
、NSApplication
など) のインスタンスを含む Objective-C ベース ランタイムと、マネージド クラス (System.String
、HttpClient
など) のインスタンスを含む C# ランタイムという、2 つの世界をブリッジします。 これら 2 つの世界の間で、Xamarin.Mac は双方向ブリッジを作成して、アプリが Objective-C の (NSApplication.Init
など) メソッド (セレクター) を呼び出し、Objective-C がアプリの C# メソッド (アプリ デリゲートのメソッドなど) を呼び出すことができるようにします。 一般的に、Objective-C への呼び出しは P/Invoke と Xamarin が提供するいくつかのランタイム コードを介して透過的に処理されます。
Objective-C への C# クラス/メソッドの公開
ただし、Objective-C が、アプリの C# オブジェクトをコールバックするには、Objective-C が理解できる方法でそのオブジェクトを公開する必要があります。 これは、Register
属性と Export
属性を介して行われます。 次に例を示します。
[Register ("MyClass")]
public class MyClass : NSObject
{
[Export ("init")]
public MyClass ()
{
}
[Export ("run")]
public void Run ()
{
}
}
この例では、Objective-C ランタイムは init
と run
というセレクターを持つ MyClass
というクラスについて認識するようになります。
ほとんどの場合、アプリが受け取るほとんどのコールバックは、base
クラス (AppDelegate
、Delegates
、DataSources
など) または API に渡されるアクションでオーバーライドされたメソッドを介して行われるため、これは開発者が無視できる実装の詳細です。 いずれの場合も、C# コードでは Export
属性は必要ありません。
コンストラクターのランスルー
多くの場合、開発者はアプリの C# クラス構築 API を Objective-C ランタイムに公開して、ストーリーボード ファイルや XIB ファイルで呼び出された場合などにインスタンス化できるようにする必要があります。 Xamarin.Mac アプリで使用される最も一般的な 5 つのコンストラクターを次に示します。
// Called when created from unmanaged code
public CustomView (IntPtr handle) : base (handle)
{
Initialize ();
}
// Called when created directly from a XIB file
[Export ("initWithCoder:")]
public CustomView (NSCoder coder) : base (coder)
{
Initialize ();
}
// Called from C# to instance NSView with a Frame (initWithFrame)
public CustomView (CGRect frame) : base (frame)
{
}
// Called from C# to instance NSView without setting the frame (init)
public CustomView () : base ()
{
}
// This is a special case constructor that you call on a derived class when the derived called has an [Export] constructor.
// For example, if you call init on NSString then you don’t want to call init on NSObject.
public CustomView () : base (NSObjectFlag.Empty)
{
}
一般的に、開発者はカスタム NSViews
アローンなどの一部の型を作成するときに生成される IntPtr
コンストラクターと NSCoder
コンストラクターを残しておく必要があります。 Xamarin.Mac が Objective-C ランタイム要求に応答してこれらのコンストラクターのいずれかを呼び出す必要があり、そのコンストラクターを削除した場合、アプリはネイティブ コード内でクラッシュし、問題を正確に把握することが困難になる可能性があります。
メモリ管理とサイクル
Xamarin.Mac でのメモリ管理は、多くの点で Xamarin.iOS とよく似ています。 また、このトピックは複雑なテーマであり、この文書の範囲を超えています。 「メモリとパフォーマンスのベスト プラクティス」をご覧ください。
Ahead Of Time コンパイル
通常、.NET アプリケーションはビルド時にマシン コードにコンパイルされません。代わりに、IL コード と呼ばれる中間レイヤーにコンパイルされ、アプリの起動時に Just-In-Time (JIT) がマシン コードにコンパイルされます。
必要なマシン コードが生成されるまでに時間がかかるため、mono ランタイムがでこのマシン コードを JIT コンパイルしている間は、Xamarin.Mac アプリの起動速度が最大 20% 低下する可能性があります。
Apple による iOS の制限のため、Xamarin.iOS では IL コードの JIT コンパイルは使用できません。 その結果、すべての Xamarin.iOS アプリは、ビルド サイクル中にマシン コードにコンパイルされた完全な Ahead-Of-Time (AOT) になります。
Xamarin.Mac の新機能として、Xamarin.iOS と同様に、アプリのビルド サイクル中に IL コードを AOT する機能があります。 Xamarin.Mac はハイブリッド AOT アプローチを採用しており、必要なマシンコードの大部分をこのアプローチでコンパイルしますが、ランタイムが必要なトランポリンをコンパイルし、Reflection.Emit (および現在 Xamarin.Mac で動作しているその他のユースケース) を引き続きサポートできる柔軟性があります。
AOT が Xamarin.Mac アプリに役立つ主な領域は 2 つあります。
- "ネイティブ" クラッシュ ログの向上 - Xamarin.Mac アプリケーションがネイティブコードでクラッシュした場合、一般的には Cocoa API に不正な呼び出し (
null
を受け付けないメソッドに送信するなど) を行った場合に発生しますが、JIT フレームを含むネイティブのクラッシュ ログを分析するのは困難です。 JIT フレームにはデバッグ情報がないため、16 進オフセットを持つ行が複数存在しているだけで、何が起こっていたか確認できる手がかりはありません。 AOT は "実際の" 名前付きフレームを生成し、トレースを読みやすくします。 また、トレースが読みやすくなるということは、Xamarin.Mac アプリが lldb や Instruments などの ネイティブ ツールとより適切に対話できることを意味します。 - 起動時間のパフォーマンスの向上 - 起動時間が数秒かかる大規模な Xamarin.Mac アプリケーションでは、すべてのコードを JIT コンパイルするとかなりの時間がかかる場合があります。 AOT は、この作業を前もって行います。
AOT コンパイルの有効化
Xamarin.Mac で AOT を有効にするには、ソリューション エクスプローラー でプロジェクト名をダブルクリックし、Mac Build に移動し、Additional mmp arguments: フィールドに --aot:[options]
を追加します ([options]
は AOT タイプを制御する 1 つ以上のオプションです。以下を参照してください)。 次に例を示します。
重要
AOT コンパイルを有効にすると、ビルド時間が大幅に増加し、場合によっては最大数分かかることもありますが、アプリの起動時間を平均 20% 向上させることができます。 その結果、AOT コンパイルは Xamarin.Mac アプリのリリース ビルドでのみ有効にする必要があります。
AOT コンパイル オプション
Xamarin.Mac アプリで AOT コンパイルを有効にするときに調整できるオプションはいくつかあります。
none
- AOT コンパイルなし。 これが既定の設定です。all
- AOT は MonoBundle 内のすべてのアセンブリをコンパイルします。core
- AOT はXamarin.Mac
、System
およびmscorlib
アセンブリをコンパイルします。sdk
- AOT はXamarin.Mac
および基底クラス ライブラリ (BCL) アセンブリをコンパイルします。|hybrid
- 上記のオプションのいずれか 1 つにこれを追加すると、ハイブリッド AOT が有効になり、IL の削除が可能になりますが、コンパイル時間が長くなります。+
- AOT コンパイル用の 1 つのファイルが含まれます。-
- AOT コンパイルから 1 つのファイルを削除します。
たとえば、 --aot:all,-MyAssembly.dll
MonoBundle except 内のすべてのアセンブリで AOT コンパイルを有効にし MyAssembly.dll
ハイブリッドを有効にする --aot:core|hybrid,+MyOtherAssembly.dll,-mscorlib.dll
、コード AOT には MyOtherAssembly.dll
が含まれており、 mscorlib.dll
は除外されます。
部分静的 registrar
Xamarin.Mac アプリを開発する場合、変更を完了してからテストまでの時間を最短にすることは、開発期限を守る上で重要になる場合があります。 コードベースや単体テストのモジュール化などの戦略は、アプリでコストのかかる完全なリビルドが必要になる回数を減らすため、コンパイル時間を短縮するのに役立ちます。
さらに、Xamarin.Mac の新機能である部分静的 Registrar (Xamarin.iOS によって開拓された) は、デバッグ構成での Xamarin.Mac アプリの起動時間を大幅に短縮できます。 部分静的 Registrar を使用することで、デバッグの起動で 5 倍近く向上されるしくみについて理解するには、registrar とは何か、静的と動的の違い、およびこの "部分静的" バージョンが何を行うかについて、少しの背景を知る必要がなります。
registrar について
Xamarin.Mac アプリケーションの内部には、Apple の Cocoa フレームワークと Objective-C ランタイムがあります。 この "ネイティブ ワールド" と C# の "マネージド ワールド" の間のブリッジを構築することが、Xamarin.Mac の主な役割です。 このタスクの一部は、NSApplication.Init ()
メソッド内で実行される registrar によって処理されます。 これが、Xamarin.Mac で Cocoa API を使用する場合に最初に NSApplication.Init
を呼び出す必要がある理由の 1 つです。
registrar の仕事は、NSApplicationDelegate
、NSView
、NSWindow
、NSObject
などのクラスから派生するアプリの C# クラスの存在を Objective-C のランタイムに通知することです。 このため、登録を必要としている内容と報告するべき各型の要素を判断するためにアプリ内のすべての型をスキャンする必要があります。
このスキャンは、リフレクションを使用してアプリケーションの起動時に動的に実行することも、ビルド時のステップとして静的に実行することもできます。 登録の種類を選択する場合、開発者は次の点に注意する必要があります。
- 静的登録は起動時間を大幅に短縮できますが、ビルド時間が大幅に遅くなる可能性があります (通常、デバッグ ビルド時間の 2 倍以上)。 これは、リリース構成ビルドの既定値になります。
- 動的登録は、アプリケーションの起動までこの作業を遅らせ、コードの生成をスキップしますが、この追加作業により、アプリケーションの起動時に顕著な一時停止 (少なくとも 2 秒) が発生する可能性があります。 これは、デバッグ構成ビルドで特に顕著であり、既定では動的登録に設定され、リフレクションが遅くなります。
Xamarin.iOS 8.13 で最初に導入された部分静的登録により、開発者はこの両方のオプションを最大限に利用できます。 Xamarin.Mac.dll
内のすべての要素の登録情報を事前に計算し、静的ライブラリ内の Xamarin.Mac でこの情報を配布することで (ビルド時にのみリンクする必要があります)、Microsoft はビルド時間に影響を与えずに、動的 registrar のリフレクション時間の大部分を削除しました。
部分静的 registrar を有効にする
Xamarin.Mac で部分静的 Registrar を有効にするには、ソリューション エクスプローラーでプロジェクト名をダブルクリックし、Mac Build に移動し、Additional mmp arguments: フィールドに --registrar:static
を追加します。 次に例を示します。
その他のリソース
内部的な動作の詳細な説明を次に示します。