dotnet-maui-aot について
iOS と Mac Catalyst を対象とする .NET MAUI アプリで、Native AOT を設定・テストするためのスキルです。発行、トリミング、ライブラリ互換性、AOT ビルドの検証を扱います。
主な機能
- 01iOS と Mac Catalyst の AOT 発行設定
- 02トリミングとリンカーディスクリプターの理解
- 03ライブラリ互換性の問題を特定
- 04リフレクションをソースジェネレーターで置換
- 05シミュレーターやデバイスで AOT ビルドをテスト
活用シーン
- 01Native AOT で MAUI アプリを発行
- 02トリミングや AOT のビルド警告を調査
- 03リフレクションベースのシリアライズを置換
- 04リリース前に AOT の動作を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-maui-aotインストール後、Claude Code で/dotnet-maui-aotを入力して読み込まれたか確認します。
詳細
dotnet-maui-aot
iOS および Mac Catalyst 向け .NET MAUI の Native AOT コンパイル:コンパイルパイプライン、発行プロファイル、アプリサイズを最大 50%削減し、起動を最大 50%高速化する方法、ライブラリの互換性に関する制約、オプトアウトの仕組み、トリミングとの相互作用(RD.xml、ソースジェネレーター)、デバイス上での AOT ビルドのテストについて説明します。
バージョンの前提: .NET 8.0 以降を基準とします。MAUI の Native AOT は iOS および Mac Catalyst で利用できます。Android では異なるコンパイルモデルを使用します(.NET 11 では CoreCLR、.NET 8~10 では Mono/AOT)。
スコープの境界: このスキルでは、iOS/Mac Catalyst 向けの MAUI 固有の Native AOT、つまりコンパイルパイプライン、発行構成、サイズと起動時間の改善、MAUI アプリでのライブラリ互換性、AOT ビルドのテストを扱います。一般的な Native AOT のパターンは [skill:dotnet-native-aot]、AOT のアーキテクチャに関する判断は [skill:dotnet-aot-architecture] が扱います。
対象外: MAUI 開発パターン(プロジェクト構成、XAML、MVVM)については、[skill:dotnet-maui-development] を参照してください。MAUI のテストについては、[skill:dotnet-maui-testing] を参照してください。WASM AOT(Blazor/Uno)については、[skill:dotnet-aot-wasm] を参照してください。一般的な AOT アーキテクチャについては、[skill:dotnet-native-aot] を参照してください。
相互参照:[skill:dotnet-maui-development] は MAUI のパターン、[skill:dotnet-maui-testing] は AOT ビルドのテスト、[skill:dotnet-native-aot] は一般的な AOT のパターン、[skill:dotnet-aot-wasm] は WASM AOT、[skill:dotnet-ui-chooser] はフレームワークの選定について説明します。
iOS/Mac Catalyst の AOT コンパイルパイプライン
iOS および Mac Catalyst の Native AOT は、発行時に .NET IL をネイティブマシンコードへ直接コンパイルするため、実行時に JIT コンパイラーや IL インタープリターが不要になります。これにより、プラットフォームのフレームワークとリンクする、自己完結型のネイティブバイナリが生成されます。
仕組み
- IL のコンパイル: NativeAOT コンパイラー(ILC)が .NET IL をネイティブコードにコンパイルします
- ツリーシェイキング: 到達可能な型とメソッドの静的解析に基づいて、未使用のコードがトリミングされます
- ネイティブリンク: 生成されたネイティブコードが iOS/Catalyst のフレームワークおよび最小限の .NET ランタイムとリンクされます
- アプリバンドル: ネイティブ実行ファイルを含む標準の
.appバンドルが生成されます(IL アセンブリは同梱されません)
発行の構成
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0-ios' Or
'$(TargetFramework)' == 'net8.0-maccatalyst'">
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
# Publish with AOT for iOS
dotnet publish -f net8.0-ios -c Release -r ios-arm64
# Publish with AOT for Mac Catalyst
dotnet publish -f net8.0-maccatalyst -c Release -r maccatalyst-arm64
# Publish for iOS simulator (for AOT testing without device)
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64
エンタイトルメントとプロビジョニング
AOT ビルドには、通常の iOS/Catalyst ビルドと同じエンタイトルメントとプロビジョニングプロファイルが必要です。AOT 専用の追加エンタイトルメントは不要です。
サイズの削減
iOS では、Native AOT によりインタープリター/JIT モードと比べてアプリサイズを最大 50%削減できます。サイズが改善される理由は次のとおりです。
- ツリーシェイキング: 最終バイナリには、到達可能なコードのみが含まれます
- IL の非同梱: アプリバンドルに .NET IL アセンブリは含まれません
- 実行時 JIT の排除: JIT コンパイラーと関連メタデータがパッケージ化されません
一般的なサイズの比較
| モード | おおよそのサイズ | 備考 |
|---|---|---|
| インタープリター(.NET 8 iOS の既定) | ~60-80 MB | IL アセンブリとインタープリターを含む |
| Native AOT | ~30-45 MB | ネイティブバイナリのみ。IL は含まれません |
| Native AOT + StripSymbols | ~25-40 MB | デバッグシンボルを削除 |
注意: 実際のサイズ削減量は、アプリの複雑さ、サードパーティライブラリの使用状況、トリミング後に到達可能なコードの量によって異なります。リフレクションを多用するライブラリは、積極的なトリミングを妨げ、サイズ削減の効果を小さくする場合があります。
起動時間の改善
Native AOT により、iOS および Mac Catalyst でコールドスタートを最大 50%高速化できます。起動時間が改善される理由は次のとおりです。
- JIT によるウォームアップが不要: コードはすでにネイティブであり、アプリの起動時にコンパイルされません
- IL の読み込みが不要: .NET アセンブリの読み込みや解析が不要です
- メモリ負荷の軽減: 起動時のワーキングセットが小さくなります
起動時間の計測
// Instrument startup timing
public partial class App : Application
{
private static readonly long StartTicks = Stopwatch.GetTimestamp();
public App()
{
InitializeComponent();
MainPage = new AppShell();
var elapsed = Stopwatch.GetElapsedTime(StartTicks);
System.Diagnostics.Debug.WriteLine(
$"App startup: {elapsed.TotalMilliseconds:F0}ms");
}
}
# Use Xcode Instruments for precise startup measurement
# Time Profiler template → measure "pre-main" + "post-main" time
# Compare AOT vs non-AOT builds on the same device
ライブラリの互換性
多くの .NET ライブラリは、AOT に完全には対応していません。よくある互換性の問題は、次のとおりです。
- リフレクション: 実行時の型検査、
Type.GetType()、Activator.CreateInstance() - 動的コード生成:
System.Reflection.Emit、System.Linq.Expressions.Compile() - ソースジェネレーターを使用しないシリアル化: リフレクションを使用する JSON/XML シリアライザー
互換性マトリックス
| ライブラリ/機能 | AOT の状態 | 回避策 |
|---|---|---|
| System.Text.Json(ソースジェネレーター) | 対応 | [JsonSerializable] コンテキストを使用 |
| System.Text.Json(リフレクション) | 問題あり | ソースジェネレーターに切り替え |
| CommunityToolkit.Mvvm | 対応 | ソースジェネレーターに基づいており、AOT セーフ |
| Entity Framework Core | 部分対応 | プリコンパイル済みクエリを使用し、動的 LINQ は使用しない |
| Newtonsoft.Json | 問題あり | ソースジェネレーターを使用した System.Text.Json に移行 |
| AutoMapper | 問題あり | Mapperly を使用(ソースジェネレーター) |
| MediatR | 部分対応 | ハンドラーを明示的に登録し、アセンブリスキャンを避ける |
| HttpClient | 対応 | 標準的な使用方法で動作 |
| MAUI Essentials | 対応 | プラットフォーム API は AOT セーフ |
| SQLite-net | 対応 | P/Invoke を使用しており、AOT セーフ |
| Refit | 問題あり | Refit 7+ を使用(ソースジェネレーターを含む。[GenerateRefitClient]` で有効化) |
| FluentValidation | 部分対応 | 実行時の式のコンパイルを避ける |
非互換コードの検出
<PropertyGroup>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>
AOT 分析では、IL3050(RequiresDynamicCode)や IL2026(RequiresUnreferencedCode)のような警告が生成されます。AOT で公開する前にこれらに対処してください。
オプトアウトの仕組み
AOT を完全に無効化する
<PropertyGroup>
<PublishAot>false</PublishAot>
</PropertyGroup>
アセンブリ単位でのトリミングの上書き
特定のライブラリが AOT に対応していない場合、アプリの残りの部分では AOT を使用しながら、そのライブラリをトリミングから保護できます。
<ItemGroup>
<TrimmerRootAssembly Include="IncompatibleLibrary" />
</ItemGroup>
.NET 11 の既定値をオプトアウトする
.NET 11 では、AOT と連動する新しい既定値が導入されます。
<PropertyGroup>
<MauiXamlInflator>XamlC</MauiXamlInflator>
</PropertyGroup>
<PropertyGroup>
<UseMonoRuntime>true</UseMonoRuntime>
</PropertyGroup>
トリミングとの相互作用
Native AOT にはトリミングが必要です。PublishAot が true の場合、トリミングは自動的に有効になります。トリミングの設定を理解することは、AOT ビルドを成功させるうえで不可欠です。
リフレクション対象を保持する ILLink 記述子
注: Xamarin/Mono 時代のドキュメントでは、これらは「rd.xml」(Runtime Directives)と呼ばれていました。.NET 8+ の Native AOT では、代わりに ILLink 記述子の XML ファイルを使用します。
トリマーが静的に分析できないリフレクションをコードで使用する場合は、ILLink 記述子の XML ファイルを使用して型を保持します。コード内で [DynamicDependency] 属性を使用して、対象を絞って保持することもできます。
ILLink 記述子の XML(まとめて保持する場合に推奨):
<linker>
<assembly fullname="MyApp">
<type fullname="MyApp.Models.LegacyConfig" preserve="all" />
<type fullname="MyApp.Services.PluginLoader">
<method name="LoadPlugin" />
</type>
</assembly>
<assembly fullname="IncompatibleLibrary" preserve="all" />
</linker>
<ItemGroup>
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
</ItemGroup>
[DynamicDependency] 属性(対象を絞って保持する場合に推奨):
using System.Diagnostics.CodeAnalysis;
// Preserve a specific method on a type
[DynamicDependency(nameof(LegacyConfig.Initialize), typeof(LegacyConfig))]
public void ConfigureApp() { /* ... */ }
// Preserve all public members of a type
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(LegacyConfig))]
public void LoadPlugins() { /* ... */ }
ソースジェネレーターによる代替
ソースジェネレーターを利用できない場合は、ILLink XML ファイルを使わずに対象を絞って保持できるよう、(上記の)[DynamicDependency] 属性を使用します。
トリミングの問題を完全に避けるため、リフレクションよりもソースジェネレーターを優先してください。
| リフレクションのパターン | ソースジェネレーターによる代替 |
|---|---|
JsonSerializer.Deserialize<T>() | [JsonSerializable] コンテキスト(System.Text.Json) |
Activator.CreateInstance<T>() | 明示的な登録を使用したファクトリーパターン |
Type.GetProperties() | CommunityToolkit.Mvvm の [ObservableProperty] |
| DI のアセンブリスキャン | services.Add*() による明示的な登録 |
| AutoMapper のリフレクションによるマッピング | Mapperly の [Mapper] ソースジェネレーター |
トリミング警告
# Build with detailed trim warnings
dotnet publish -f net8.0-ios -c Release /p:PublishAot=true /p:TrimmerSingleWarn=false
# TrimmerSingleWarn=false shows per-occurrence warnings instead of
# one summary warning per assembly, making it easier to fix issues
よくあるトリミング警告:
- IL2026:
RequiresUnreferencedCodeを持つメンバー。トリミング後に動作する保証がない処理を実行します。 - IL2046: 基底型と派生型の間でトリミング属性が一致しません。
- IL3050:
RequiresDynamicCodeを持つメンバー。実行時にコードを生成するため、AOT と互換性がありません。
AOT ビルドのテスト
AOT ビルドは、Debug/JIT ビルドとは異なる動作をすることがあります。リリース前に、AOT で公開したビルドを実機またはシミュレーターで必ずテストしてください。
AOT のみで発生する一般的な失敗
| 失敗 | 症状 | 対処法 |
|---|---|---|
| 型メタデータの欠落 | 実行時の MissingMetadataException | 型を ILLink 記述子に追加するか、[DynamicDependency] を使用します |
| メソッドのトリミング | MissingMethodException | [DynamicDependency] または ILLink 記述子エントリを追加します |
| 動的コード生成 | PlatformNotSupportedException | ソースジェネレーターによる代替手段に置き換えます |
| リフレクションベースのシリアル化 | デシリアライズされたオブジェクトが空または null になる | [JsonSerializable] によるソース生成を使用します |
| アセンブリのスキャン | 実行時にサービスが見つからない | DI でサービスを明示的に登録します |
テストのワークフロー
# 1. Build and publish with AOT for simulator (faster iteration)
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64
# 2. Install and test on simulator
# (Use Xcode or Visual Studio to deploy the .app to simulator)
# 3. Run smoke tests -- focus on:
# - App startup (no MissingMetadataException)
# - JSON deserialization (all properties populated)
# - Navigation (all pages render)
# - Platform services (biometric, camera, location)
# - Third-party SDK integration
# 4. Test on physical device before release
dotnet publish -f net8.0-ios -c Release -r ios-arm64
# Deploy via Xcode with provisioning profile
CI との統合
# CI pipeline: build AOT and run device tests via XHarness
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64 /p:PublishAot=true
xharness apple test \
--app bin/Release/net8.0-ios/iossimulator-arm64/publish/MyApp.app \
--target ios-simulator-64 \
--timeout 00:10:00 \
--output-directory test-results/aot
MAUI のテストパターン(Appium、XHarness)については、[skill:dotnet-maui-testing] を参照してください。
エージェント利用時の注意点
- トリムアナライザーを有効にせずに
PublishAotを有効にしないでください。 AOT にはトリミングが必要です。問題を早期に検出できるよう、開発中は<EnableTrimAnalyzer>true</EnableTrimAnalyzer>と<EnableAotAnalyzer>true</EnableAotAnalyzer>を設定してください。 - すべての NuGet パッケージが AOT に対応していると思い込まないでください。 パッケージの
.csprojでIsAotCompatibleを確認するか、ビルド時にトリミング/AOT の警告が出るか確認してください。現在も、多くの人気パッケージが内部でリフレクションを使用しています。 - AOT で
Newtonsoft.Jsonを使用しないでください。 これは全面的にリフレクションに依存しています。AOT に安全なシリアル化を行うには、System.Text.Jsonと[JsonSerializable]によるソース生成コンテキストに移行してください。 - AOT ビルドのデバイステストを省略しないでください。 シミュレーターのテストでほとんどの問題は検出できますが、特に起動時間、メモリ制約、プラットフォームサービスとの統合では、実機の動作が異なる場合があります。
- MAUI の iOS AOT と Android AOT を混同しないでください。 MAUI Native AOT(
PublishAot)の対象は iOS と Mac Catalyst のみです。Android では別のコンパイルモデル(.NET 8~10 では Mono AOT、.NET 11 以降では CoreCLR)が使用されます。これらは別々に構成します。
前提条件
- MAUI ワークロードを含む .NET 8.0 以降
- Xcode と iOS/Mac Catalyst SDK(macOS のみ)
- 実機にデプロイするための Apple Developer アカウント
- デバイステスト用のプロビジョニングプロファイルと署名証明書