Start Debugging

.NET 11 で .NET MAUI の Android アプリを Mono から CoreCLR へ移行する

Android 向け .NET MAUI を Mono から CoreCLR へ移行する手順ガイドです。API 24 という下限、ビルドを壊すようになった Mono 専用の MSBuild プロパティ、APK が大きくなった理由、dotnet-dsrouter と dotnet-trace による起動リグレッションのプロファイリング、そして Mono の経路が失われた今のロールバックの実態を扱います。

小規模なアプリであれば、この移行は TargetFramework の変更と android:minSdkVersion の変更、そして半日の計測で終わります。大規模なアプリなら 1 週間を見込んでください。しかもその 1 週間は、ほぼ次の 2 つに消えます。何もしないか、あるいは積極的にビルドを壊すようになった Mono 時代の MSBuild プロパティを削除すること。そして、あなたのコードとは無関係な起動リグレッションを追いかけることです。見返りは本物です (統一されたデバッグ/診断、階層化 JIT、動的 PGO、Android における Native AOT への現実的な道筋)。ただし正直に言えば、これは選択肢ではありません。.NET 11 Preview 6 以降、Microsoft は Android、iOS、Mac Catalyst 向けに Mono の独立した経路を提供しなくなりました。本ガイドは .NET 11 Preview 7 (11.0.100-preview.7、2026-08-11 リリース) と .NET MAUI 11.0.0-preview.7 を対象とし、Mono 上の .NET 10 からの移行を扱います。.NET 11 の正式版は 2026-11-10 に予定されています。

「選択肢がない」以外にこれをやる価値

何が壊れるか

領域変更深刻度
Android の最小 API21 (Android 5.0) から 24 (Android 7.0) へ引き上げ
Android の ABIAndroid x86 (32 ビット) は CoreCLR ではサポート対象外
Mono AOT のプロパティRunAOTCompilationAndroidAotModeUseInterpreter は Mono 専用。RunAOTCompilation=true は今も MonoAOTCompiler を呼び出してビルドを失敗させることがある
起動時間大規模アプリで数秒規模のリグレッションと ANR が報告されている高 (状況依存)
APK サイズR2R イメージは .dll ファイルの内部に置かれるため、アセンブリが大きくなる
NuGet パッケージパッケージが net6.0-android 以降ではなく MonoAndroid のアセットに解決されると NU1703 が出る
旧来のリソース依存関係に埋め込まれた旧来の Xamarin.Android リソースに対する XA0149
Microsoft.Maui.Controls.CompatibilityPreview 6 でパッケージが削除中 (明示的に参照している場合のみ)
HTTP エラーAndroidMessageHandler の転送失敗が WebException ではなく HttpRequestException をスローする
ランタイムの埋め込みAndroid の埋め込み API は CoreCLR には引き継がれない高 (使用している場合)

API レベルの下限は、ユーザーにまで届く変更です。破壊的変更の告知によれば、.NET 11 でビルドしたアプリは API 21、22、23 ではインストールも実行もできません。着手する前に Play Console の配信内訳を確認してください。これはビルド設定ではなく、ユーザーについての意思決定です。

事前チェックリスト

移行手順

  1. Mono のベースラインを取得します。 現行の .NET 10 Release ビルドで APK をインストールし、Android のアクティビティマネージャーでコールドスタートを計測します。TotalTime がミリ秒で出力されます。

    # .NET 10, Mono, Release
    adb shell am force-stop com.example.myapp
    adb shell am start -W -n com.example.myapp/crc64...MainActivity

    5 回実行し、1 回目を捨てて中央値を記録します。Release の APK または AAB のサイズも記録してください。検証: ターミナルのスクロールバック以外の場所に、2 つの数値が書き留められていること。

  2. ターゲットフレームワークと API の下限を一緒に動かします。 CoreCLR on Android は API 24 を要求するため、両方の変更を 1 つのコミットで行います。

    <!-- .NET 11 Preview 7, MAUI 11.0.0-preview.7 -->
    <PropertyGroup>
      <TargetFrameworks>net11.0-android;net11.0-ios;net11.0-maccatalyst</TargetFrameworks>
      <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
    </PropertyGroup>

    Platforms/Android/AndroidManifest.xmlandroid:minSdkVersion を手書きしている場合は、プロジェクト設定と一致するよう 24 に引き上げてください。検証: dotnet build -f net11.0-android -c Release が成功し、生成されたマニフェストに minSdkVersion="24" が表示されること。

  3. Mono 専用の MSBuild プロパティをすべて削除するか、条件付きにします。 .csprojDirectory.Build.props、そして CI が注入するプロパティを RunAOTCompilationAndroidAotModeAndroidEnableProfiledAotUseInterpreterUseMonoRuntime で grep してください。Directory.Build.propsRunAOTCompilation=true が残っているのは既知のビルド破壊です。アプリが CoreCLR 上にあっても MonoAOTCompiler ターゲットが実行されてしまいます (dotnet/android#11068)。丸ごと削除するか、古い TFM を並行してビルドしているなら条件を付けてください。

    <PropertyGroup Condition="'$(UseMonoRuntime)' == 'true'">
      <RunAOTCompilation>true</RunAOTCompilation>
      <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
    </PropertyGroup>

    検証: dotnet build -f net11.0-android -c Release -bl を実行し、バイナリログを MonoAOTCompiler で検索します。ヒット 0 件が合格条件です。

  4. ABI の一覧とパッケージ警告を片付けます。 CoreCLR は配布しないので、RuntimeIdentifiersx86 が残っていれば外します。

    <RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>

    次に NU1703 に対処します。Preview 5 で導入された警告で、パッケージが非推奨の MonoAndroid フォルダーのアセットに解決されたときに出ます: “Package ‘PackageName’ 1.0.0 uses the deprecated MonoAndroid framework instead of ‘net6.0-android’ or later.” 新しいバージョンがあればパッケージを更新してください。存在しないなら、それは残り時間が限られた Xamarin 時代の依存関係を見つけたということであり、警告の抑制は修正ではなくそのリスクを引き受ける意思決定です。検証: dotnet restore がクリーンであること。あるいは残っている NU1703 がすべて、意識的にトリアージ済みのパッケージであること。

  5. Release で再ビルドし、手順 1 と比較して再計測します。 同じ実機、同じ手順、同じ実行回数で行います。

    # .NET 11 Preview 7, CoreCLR, Release
    dotnet publish -f net11.0-android -c Release
    adb install -r bin/Release/net11.0-android/publish/com.example.myapp-Signed.apk
    adb shell am force-stop com.example.myapp
    adb shell am start -W -n com.example.myapp/crc64...MainActivity

    Microsoft 自身の見解では、テンプレートのベースラインアプリにおいて Android は起動とアプリサイズで「Mono の 10 パーセント以内」に収まっています。検証: この範囲に入っていればパフォーマンス作業は完了です。2 倍以上悪いなら、MSBuild のプロパティを手当たり次第に切り替えるのではなく手順 6 へ進んでください。

  6. 推測せずにリグレッションをプロファイルします。 .csproj の隣に DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend を書いた app.env ファイルを置き、条件付きで参照します。

    <ItemGroup Condition="'$(AndroidEnableProfiler)'=='true'">
      <AndroidEnvironment Include="app.env" />
    </ItemGroup>

    ルーターを起動し、プロファイラーを有効にしてビルドし、アプリを起動してから接続します。

    dotnet-dsrouter server-server -ipcs ~/mylocalport -tcps 127.0.0.1:9000 --forward-port Android &
    dotnet build -f net11.0-android -c Release -t:Run /p:AndroidEnableProfiler=true
    dotnet-trace collect --diagnostic-port ~/mylocalport,connect

    ポートを suspend で構成しているので、ランタイムは dotnet-trace が接続するまで起動時にブロックします。起動後のすべてではなく起動パスそのものを見たいときに、まさに必要な挙動です。Windows では IPC チャネルが名前付きパイプになるため、~/mylocalport ではなく mylocalport を使ってください。検証: 起動区間が埋まった .nettrace ファイルがあり、包括時間の上位 3 メソッドを名前で挙げられること。

  7. トレースが正当化するものだけを調整します。 アセンブリサイズが問題なら、最初に触るつまみは R2R です。R2R イメージは .dll ファイルの中に詰め込まれており、それがアセンブリの肥大化の理由だからです。

    <PropertyGroup Condition="'$(Configuration)' == 'Release'">
      <PublishReadyToRun>false</PublishReadyToRun>  <!-- smaller APK, slower startup -->
      <TrimMode>full</TrimMode>                     <!-- default is partial -->
    </PropertyGroup>

    この 2 つは逆方向に働きます。R2R を切ると起動時間とサイズを交換することになり、TrimMode=full はサイズを取り戻す代わりに、あなた自身のコードと NuGet 参照までトリミングするため、フルのリグレッションテストが必要になります。1 つずつ変更し、その都度手順 5 をやり直してください。検証: どのつまみも、ブログ記事ではなく自分で引用できる計測差分によって正当化されていること。

  8. 段階的にロールアウトします。 まず内部トラックに公開し、クラッシュ率だけでなく ANR 率を特に注視します。大規模アプリで報告されている CoreCLR の障害モードは、Android がプロセスを強制終了するほど起動が長引くというもので、例外ではなく ANR として現れます。検証: 1 週間の内部テスト後、Play Console の ANR 率が Mono ビルドと比べて横ばいであること。

検証チェックリスト

ロールバック計画

はっきり言っておきます。ランタイムレベルのロールバックはもう存在しません。 <UseMonoRuntime>true</UseMonoRuntime> は Preview 4 で CoreCLR が既定になったときの脱出ハッチとして文書化されましたが、その時点でも、リグレッションを報告する間の一時的な回避策という位置づけでした。Preview 6 は Android、iOS、Mac Catalyst 向けの独立した Mono 経路を削除しました。このプロパティは消えたものとして扱い、リリース計画をその上に組み立てないでください。

実際のロールバック手段はターゲットフレームワークです。.NET 11 のビルドが本番の実ロールアウトを乗り切るまで、net10.0-android のビルドをブランチでグリーンに保っておいてください。プロパティを 1 つ切り替えるよりずっと重いロールバックであり、だからこそ手順 1 と手順 5 が存在します。

実際に時間を奪う落とし穴

起動リグレッションは実在し、しかも一様ではありません。 障害モードを記録した issue が 2 件あります。dotnet/android#10588 は “an app that takes 1s to launch on mono can take 6s on coreclr” と報告し、Avalonia の ControlCatalog.Android での ANR を挙げています。dotnet/android#1091411.0.100-preview.2 でコールドスタートがおよそ 1.0 秒から 6.0 秒へ、APK が 21 MB から 38 MB へ増えたと報告しています。どちらも MAUI ではなく Avalonia であり、どちらも composite partial R2R と MIBC プロファイルの作業がプレビュー期間の後半に入る前のものです。ですから、これらを自分の期待値として読まないでください。手順 1 が必須である理由として読んでください。

痛むのは XAML の比重が大きい起動パスです。 報告に共通するのは初期化中のリフレクションと XAML の解析であり、これはまさに、同梱の .mibc プロファイルがあなたのアプリの形をカバーしていない場合に partial R2R が事前コンパイルできない処理です。最初のフレームより前に大きなビジュアルツリーを構築するアプリなら、まずそこを見てください。

UseInterpreter は静かに無意味になります。 Mono では Debug で既定が true であり、Mono 時代の Hot Reload を成立させていたのがこれでした。CoreCLR では不活性です。何か理由があって有効にしていたのなら (Mono AOT で扱えない動的なコードパスなど)、その理由は消えたのではなく移動しただけです。CoreCLR on Android は Debug で本物の JIT を動かすのでコードは動きますが、そう決めつけず意識的に再テストしてください。

APK の中身は形が変わります。 Mono では libmonosgen-2.0.solibaot-*.dll.so イメージを同梱していました。CoreCLR では libcoreclr.solibclrjit.solibmonodroid.so (Android のグルーコードは Mono 時代の名前のままです)、そして R2R イメージ付きの圧縮 MSIL を格納した単一の libassemblies.arm64-v8a.so を同梱します。これらのファイル名を参照するビルドスクリプト、サイズ予算、ProGuard/R8 の設定があるなら、更新が必要です。

サイズの本体はトリミングにあります。 MAUI は今も既定で TrimMode=partial を使い、フレームワークのアセンブリはトリミングしますが、あなたのコードと NuGet 参照はそのまま残します。サイズに関する不満の大半は、アセンブリ単位の内訳を見た途端にトリミングの話に変わります。

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る