.NET MAUIでデバッグは動くのにReleaseで落ちる原因と対策(AOT/トリミング/Color Converter切り分け)

.NET MAUIで「デバッグ実行は正常なのに、Release(非デバッグ)だと起動直後に無言で終了する」症状は、AOTだけに原因があるとは限りません。AOT無効化が効かなかった実例と、最終的にColor Converterの差し替えで解決した経緯をもとに、切り分けと再発防止をまとめます。

目次

現象:Debugでは動くのにReleaseで起動直後に落ちる(例外メッセージなし)

今回のケースは、.NET MAUIアプリがDebug実行だと正常動作する一方で、Release実行(非デバッグ)では起動後すぐ終了してしまう、というものです。厄介なのは、ユーザー操作の前に落ちる上に、画面上に例外も出ず、Visual Studioの出力にも目立ったエラーが出ないことがある点です。

項目内容(今回の状況)読み取れること
症状DebugはOK/Releaseは起動直後に無言で終了Releaseで有効になる最適化・トリミング・構成差分が疑わしい
試したことAOT無効化のプロパティをcsprojへ追加「AOTが怪しい」という仮説で設定を変更
例外画面表示なし・メッセージなしログを取りにいかないと原因が見えない可能性
環境Windows 11/Visual Studio 17.13.2/.NET 8・.NET 9/MAUI 9.0.14 → 9.0.40ワークロードやSDK、VSの状態の影響もあり得る

「Releaseでだけ落ちる」問題は、原因が1つとは限りません。特にMAUIはXAML・リソース・バインディング・プラットフォーム差分が絡みやすく、さらにReleaseは最適化やトリミング(未使用コード削除)が影響することがあります。

まず整理:DebugとReleaseで何が違うのか

闇雲に設定をいじる前に、DebugとReleaseの「差分」を把握すると切り分けが速くなります。

差分ポイントDebugで起きがちなことReleaseで起きがちなこと代表的な症状
最適化(最適化ON/OFF)最適化が弱く、挙動が素直最適化が強く、未定義動作・タイミング差が顕在化タイミング依存のクラッシュ/初期化順の問題
ログ・例外の見え方デバッガーが例外で止まる未処理例外がそのままプロセス終了、画面に出ないことも「無言で落ちる」
トリミング(リンク)無効・弱いことが多い有効になる構成・ターゲットがある(特にモバイル)反射・XAML・コンバーターが効かない/ページが開かない
AOT(事前コンパイル)通常は無効・一部のみターゲットによって有効化され得る特定端末だけ落ちる/起動直後に終了など

ここで重要なのは、「Releaseだけ落ちる」=「AOTが原因」ではないという点です。AOTは確かに有力候補になり得ますが、同じくらいトリミング、XAML、コンバーター、依存ライブラリ、開発環境が原因になり得ます。

今回の結論:AOTが原因ではなかった

今回のやり取りでは、AOTを疑って以下のような設定を試したものの改善しませんでした。

  • <RunAOTCompilation>False</RunAOTCompilation>
  • <EnableAOTCompilation>False</EnableAOTCompilation>
  • <AotAssemblies>False</AotAssemblies>

しかし回答側で、質問者のGitHubプロジェクトをそのままWindows / Android のReleaseビルドで検証したところ、特に設定を変えなくても正常動作した、という結果でした。さらに、Microsoft.Maui.Controls9.0.40へ更新しても問題が再現しなかったため、AOT起因よりも質問者の開発環境(Visual Studio / SDK / Workload / キャッシュなど)側に原因がある可能性が高い、という判断になります。

この時点での現実的な対処としては、次のような「環境メンテ」が有力です。

  • Visual Studioを最新へ更新
  • Visual Studio Installerから修復(Repair)を実行
  • .NET SDK / MAUI Workloadの更新・修復

最終的な自己解決:Color Converterを差し替えたらReleaseでも動いた

そして決定打になったのが、質問者側での自己解決です。試行錯誤の末、外部からコピーした「色変換用コンバーター(Color Converter)」を使うと、Releaseでも正常に動作するようになった、という結論でした。

状況としては、特定ページ(DetailPage)がRelease時だけ表示されないという現象があり、コンバーターを差し替えると改善したため、XAMLで使っていたカラー変換・バインディング周りがRelease構成で問題を起こしていた可能性が高い、という推測が成り立ちます。

疑っていたこと実際に起きていたこと学び
AOTが原因でReleaseが落ちる別環境ではReleaseでも正常動作「自分のPCだけ再現」は環境・キャッシュ・SDK差分も疑う
csprojでAOTを切れば直るAOT無効化設定を入れても改善しない設定の有効範囲(構成/TFM/プラットフォーム)確認が必要
原因不明の無言終了コンバーター差し替えで改善「XAML/Converter/Resource」を最小化して切り分けるのが最短

このケースは、AOT云々よりもむしろ「Release時にだけ顕在化するXAML・コンバーターの不整合」という、MAUIでよくあるタイプに近いです。

「無言で落ちる」を終わらせる:まずログを取れる状態にする

Releaseで落ちる問題は、原因を推測するより「ログを出して事実を見る」ほうが圧倒的に早いです。特に、デバッガーが付いていないと例外ダイアログが出ないケースでは、ログが唯一の手がかりになります。

Windows(WinUI)での確認ポイント

  • イベントビューアー:Windowsログ → アプリケーション で .NET Runtime / Application Error を確認
  • Visual Studioの出力:Releaseで起動した際の出力ウィンドウを確認
  • クラッシュの瞬間の例外:未処理例外が記録されていないかを見る

Androidでの確認ポイント(adb logcat)

Androidは、画面に何も出なくてもログには出ていることが多いです。Releaseビルドで落ちるなら、次を優先します。

  • adb logcatでアプリ起動直後の例外を追う(特に AndroidRuntime / FATAL EXCEPTION
  • ビルド・署名の違いで落ちる場合、パーミッションやリソース参照エラーも疑う

Releaseでも例外を「必ずログに残す」最小の仕込み

アプリ全体の未処理例外を拾うだけでも、「無言終了」から脱出できる確率が上がります。MAUIアプリの起動時(例:MauiProgram.CreateMauiApp() 付近や App の初期化)で、最低限以下を仕込みます。

using System.Diagnostics;

public static class CrashLog
{
    public static void Register()
    {
        AppDomain.CurrentDomain.UnhandledException += (s, e) =>
        {
            Write("UnhandledException", e.ExceptionObject as Exception);
        };

        TaskScheduler.UnobservedTaskException += (s, e) =>
        {
            Write("UnobservedTaskException", e.Exception);
            e.SetObserved();
        };
    }

    private static void Write(string title, Exception? ex)
    {
        try
        {
            var msg = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {title}\n{ex}\n";
            Debug.WriteLine(msg);

            // 必要ならファイルにも書く(WindowsならLocalAppDataなど)
            // File.AppendAllText(path, msg);
        }
        catch
        {
            // ログすら書けない状況でも落とさない
        }
    }
}

そして起動時に呼び出します。

public static MauiApp CreateMauiApp()
{
    CrashLog.Register();

    var builder = MauiApp.CreateBuilder();
    // ...
    return builder.Build();
}

これで「落ちる瞬間に何が起きたか」を追いやすくなります。特にXAMLの初期化例外は、デバッガーなしだと見逃されやすいので効果的です。

切り分けの鉄則:Release固有の要素を“1つずつ”OFFにして戻す

Debug/Release差分の切り分けは、犯人探しを「推理」から「実験」に変えるのがコツです。次の表の順で潰すと、遠回りしにくくなります。

手順やること狙い判断
1別PC/別環境で同じプロジェクトをRelease実行環境依存かコード依存かを即判定別環境で動くなら「自環境or構成差分」が濃厚
2bin/obj削除→クリーンビルドキャッシュ・古い生成物を排除直れば生成物やキャッシュ汚染が原因
3Releaseでトリミングを一時OFFリンク/トリミング起因の不具合を確認OFFで直るならトリミング対策が必要
4ReleaseでAOT関連を一時OFFAOT起因の可能性を確認OFFで直るならAOT/ネイティブ周りを深掘り
5落ちるページ(例:DetailPage)のXAMLを最小化XAML/Resource/Converterのどれが犯人か切り分け最小化で直るなら、戻しながら犯人を特定

今回の自己解決は、まさに「手順5」を実行した結果に近いです。ページ単位で疑わしい要素(特にConverter)を外し、戻しながら特定するのが最短でした。

AOTを疑う場合:設定は“対象構成”に正しく入れる

AOTを疑って一時的に無効化したい場合は、Debug/Releaseのどちらに効かせたいのかを明確にした上で、.csprojに構成条件付きで入れます。

<PropertyGroup Condition="'$(Configuration)'=='Release'">
  <RunAOTCompilation>false</RunAOTCompilation>
  <EnableAOTCompilation>false</EnableAOTCompilation>
  <AotAssemblies>false</AotAssemblies>
</PropertyGroup>

ただし注意点があります。

  • プロパティ名や有効範囲は、ターゲット(Windows/Android/iOS)やSDKの世代で変わることがあります。
  • 「設定を書いたのに効かない」場合、そもそもそのターゲットでは参照されないプロパティである可能性があります。
  • 反映されているかは、ビルドログ(詳細ログ)や生成されたMSBuildプロパティで確認するのが確実です。

今回のケースでは、AOTを切っても改善しなかったため、AOT起因ではない(少なくとも主要因ではない)と判断できます。

Releaseだけ落ちるなら「トリミング(リンク)」を最優先で疑う

MAUIでReleaseだけ不調になるとき、AOTより先に疑うべき代表格がトリミング(未使用コード削除)です。特に以下のような要素は、トリミングと相性が悪いことがあります。

  • 反射(Type.GetTypeActivator.CreateInstanceなど)
  • XAMLから動的に生成されるクラス(Converter、MarkupExtensionなど)
  • 外部ライブラリが内部で反射を使っている

切り分けとして、まずは一時的にトリミングをOFFにして挙動が変わるか確認します。

<PropertyGroup Condition="'$(Configuration)'=='Release'">
  <PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>

これで直るなら、次は「トリミングをONに戻しつつ、消されると困る型・メンバーを守る」方向に進みます。守り方はいくつかありますが、現場でやりやすいのは次のどれかです。

対策概要向いているケース注意
コードから明示参照する使う型をコード上で直接参照し、トリマーに「必要」と認識させるConverterやServiceなど、数が少ない参照漏れがあると再発
トリミング注釈(属性)を付ける動的アクセスするメンバーを残す指定を行う反射を避けられない属性の理解が必要、適用範囲に注意
設定を段階的に弱めるTrimモードやリンク範囲を調整して影響を減らす短期的に出荷を優先したいアプリサイズ増加、効果が限定的なことも

今回の自己解決(Color Converter差し替え)は、「トリミングに弱い実装を、トリミング耐性のある実装に置き換えた」可能性も十分あります。

要注意:Color Converter / バインディングがReleaseでだけ壊れる典型パターン

「特定ページ(例:DetailPage)がReleaseでだけ表示されない」「起動直後に落ちる」といった症状は、XAML初期化時に例外が起きているパターンがよくあります。特にConverter周りは、次の落とし穴が多いです。

戻り値の型が一致していない

たとえば、プロパティがColorを要求しているのに、ConverterがstringBrushを返してしまう。Debugでは気付きにくくても、ReleaseではXAMLコンパイルや最適化の影響で例外が表面化することがあります。

nullや例外を握りつぶしている

Converter内部で例外が起きているのに、catchして何も出さずにnullを返す、など。結果としてページ初期化が失敗し、アプリが終了することがあります。少なくとも切り分け中は、catchしたらログに吐くのが安全です。

XAMLから動的生成されるConverterがトリミングで消える

ConverterをXAMLのリソースとして定義している場合、構成や環境によっては「参照が弱い」と判断されて削除される可能性があります。特に名前空間指定・型解決に癖があると、Releaseでだけ「型が見つからない」→初期化例外、になりがちです。

自作の色変換が“プラットフォーム差分”を踏んでいる

MAUIの色はMicrosoft.Maui.Graphics.Colorが中心ですが、うっかりSystem.Drawing系やWindows専用APIに依存した変換をすると、ターゲットや構成差で破綻しやすくなります。Debugでは偶然動いても、Releaseで落ちる引き金になります。

安全寄りのColor Converter例(自作するならここから始める)

自作Converterを使うなら、まずは「変換が失敗しても落とさない」「ログで追える」「戻り値の型が明確」という形にしておくと、Releaseクラッシュの芽を摘みやすいです。

using System.Globalization;
using Microsoft.Maui.Graphics;

public sealed class HexToColorConverter : IValueConverter
{
    // #RRGGBB / #AARRGGBB を想定(必要に応じて拡張)
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            if (value is null) return Colors.Transparent;

            var s = value.ToString()?.Trim();
            if (string.IsNullOrEmpty(s)) return Colors.Transparent;

            // MAUIのColor.FromArgbは "#RRGGBB" / "#AARRGGBB" を扱える
            if (s[0] != '#') s = "#" + s;

            return Color.FromArgb(s);
        }
        catch (Exception ex)
        {
            // 切り分け中は必ずログへ
            System.Diagnostics.Debug.WriteLine(ex);
            return Colors.Transparent;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => throw new NotSupportedException();
}

XAMLでの利用例です。

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:YourApp.Converters">

  <ContentPage.Resources>
    <ResourceDictionary>
      <local:HexToColorConverter x:Key="HexToColorConverter" />
    </ResourceDictionary>
  </ContentPage.Resources>

  <Label
      Text="Sample"
      TextColor="{Binding AccentHex, Converter={StaticResource HexToColorConverter}}" />

</ContentPage>

ポイントは次の3つです。

  • Converterはpublic / sealedでシンプルに(トリミングや型解決の事故を減らす)
  • 失敗時は例外を握りつぶさずログへ(Releaseでも追える)
  • 戻り値は対象プロパティの型に合わせる(Colorを要求するならColorを返す)

DetailPageだけ出ないときの現場的チェックリスト

「アプリ全体が落ちる」というより、「あるページへ遷移した瞬間に落ちる」「そのページだけ表示されない」場合は、次の順で当たりを付けると効率が良いです。

チェック項目確認方法よくある原因
InitializeComponentで例外が出ていないか未処理例外ログ/Debugでそのページだけ起動XAML構文、Resource解決、Converter型解決
ResourceDictionaryのキー重複や参照ミス当該ページのResourcesを一旦空にして動くかStaticResourceが見つからない、順序依存
Converterを全コメントアウトConverter使用箇所を外し、表示できるか型不一致、例外、トリミングで型が消える
BindingContext/データがnullページ生成直後に値をログ出力DI設定ミス、遷移引数の欠落
Release時のみの条件分岐#if DEBUG を検索Debug専用コードに依存している

今回のように「コンバーターを差し替えたら直った」場合、原因はConverterそのものだけでなく、Converterが依存しているAPI・型・リソース・NuGetの組み合わせであることもあります。だからこそ、ページ単位で最小化して戻す手順が強力です。

環境起因を疑うなら:Visual Studio / Workload / キャッシュを整える

別環境では動くのに自環境だけReleaseで落ちる場合、コードより先に開発環境の整合性を疑う価値があります。特にMAUIはワークロードやSDKの組み合わせが多く、アップデート途中で壊れることもあります。

よく効くメンテ手順

  • Visual Studioを最新へ更新(可能なら17.13系以外も含め最新状態へ)
  • Visual Studio Installerの修復(Repair)を実行
  • bin/obj削除 → クリーンビルド
  • NuGetキャッシュをクリア(依存の取り違えを防ぐ)
  • .NET workloadの更新・修復

コマンド例です(必要なものだけでOKです)。

dotnet --info
dotnet workload list
dotnet workload update
dotnet workload repair
dotnet nuget locals all --clear

そして、MAUI関連のNuGet(特にMicrosoft.Maui.Controlsなど)を更新した場合は、必ずクリーンな状態でビルドし直します。古い生成物が残ったままだと、更新したつもりでも症状が変わらないことがあります。

まとめ:AOTだけに固執せず、Release差分を手順で潰す

  • 「Debugは動くがReleaseで無言終了」は、AOT以外(トリミング、XAML、Converter、環境)の可能性が高い。
  • 今回の実例では、別環境では正常動作し、AOT無効化でも改善せず、最終的にColor Converter差し替えで解消した。
  • 再現が難しいほど、まずはログを取れる状態にして、「無言」をやめる。
  • 切り分けは、トリミングOFF→AOT OFF→ページ最小化→1つずつ戻すの順が効率的。
  • 自環境だけ怪しいなら、Visual Studio更新・修復、Workload更新、キャッシュ掃除を優先する。

Releaseクラッシュは焦るポイントですが、手順さえ組めれば「原因の見える化」→「小さく再現」→「対策の固定化」まで持っていけます。特にConverterやResourceは、ページ単位で切り分けできるので、疑わしいときほど最小構成に戻すのが最短ルートです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次