.NET MAUI の Android アプリで AndroidX SplashScreen を使っているのに、実行時に androidx.core.splashscreen.SplashScreen が見つからない(ClassNotFoundException)でクラッシュする――この症状は「参照が足りない」よりも、縮小/最適化(R8/ProGuard)でクラスが消されているケースが多いです。この記事では原因の見分け方と、Debug は安定優先・Release だけ最適化する実践的な設定に落とし込みます。
.NET MAUI(Android)で発生するエラーの特徴
今回の現象は、コンパイルは通るのに実行時だけ落ちるのがポイントです。典型的には Activity.OnCreate() 内で次の呼び出しを入れた瞬間、起動直後にクラッシュします。
AndroidX.Core.SplashScreen.SplashScreen.InstallSplashScreen(this);
ログには次のような内容が出ます(文言は端末やビルド条件で多少変わります)。
Java.Lang.ClassNotFoundException:
Didn't find class "androidx.core.splashscreen.SplashScreen" ...
そして発端として多いのが、質問文にあるように Xamarin.Firebase.Crashlytics(NuGet)を入れた/外した後から急に起き始めるパターンです。ここで重要なのは、Crashlytics 自体が直接 SplashScreen を壊すというより、ビルド最適化まわり(R8/ProGuard/設定ファイル参照)を“有効にする条件”や“参照するファイル”が変化しやすい点です。
まず確認する必須要件:AndroidX SplashScreen 参照の有無
InstallSplashScreen() を使うには、AndroidX の SplashScreen ライブラリが必要です。.NET MAUI(Android)では基本的に NuGet で Xamarin.AndroidX.Core.SplashScreen を参照に入れます。
未参照なら話はシンプルで、追加すれば解決することが多いです。問題は「もう入っているのに落ちる」ケースで、これは次章の“縮小/最適化で消された”が本命になります。
| 状況 | 起きやすい症状 | まずやること |
|---|---|---|
| 参照が入っていない | ビルドエラー/型が解決できない | NuGet に Xamarin.AndroidX.Core.SplashScreen を追加 |
| 参照は入っている | 実行時に ClassNotFoundException | R8/ProGuard による削除を疑う |
「入っているのに見つからない」本当の原因:縮小/最適化でクラスが削除される
ClassNotFoundException は名前の通り「そのクラスが実行時にロードできない」状態です。ここで混同されがちなのが、Android には大きく分けて2種類の“削る仕組み”がある点です。
| 仕組み | 対象 | 削りすぎると起きること | 今回の例との関係 |
|---|---|---|---|
| リンク(Linker/トリミング) | C#(.NET/IL)側 | 必要な型がトリムされて例外/機能欠落 | 主にManaged 側の不具合として出やすい |
| 縮小(R8/ProGuard) | Java/Dex 側(AndroidX 等) | Dex にクラスが入らず ClassNotFoundException | まさに今回の androidx.core.splashscreen が該当 |
つまり、NuGet 参照は確かにあるのに、最終的な APK/AAB からそのクラスが消えてしまった(または別の理由で含まれなくなった)というのが筋の通った説明になります。
トリガーになりやすい設定:csproj の <AndroidLinkTool>r8</AndroidLinkTool>
質問のとおり、最終的に <AndroidLinkTool>r8</AndroidLinkTool> を消したら例外が出なくなった場合、かなり高い確度でR8 が強い最適化をかけて必要なクラスまで削除していた可能性が高いです。
ここで気になるのが「消してよいか?」ですが、結論から言うと次の整理が現実的です。
- Debug(開発・検証):安定して動くことが最優先。R8 を常時走らせるメリットは小さく、むしろ切り分けが難しくなるため、無効化/未指定で運用するのが安全。
- Release(配布):サイズ削減や最適化のメリットが出る。必要なら R8 を使う。ただし消されたら困るクラスは keep ルールで保護する。
| 判断軸 | Debug で R8 を使う | Release で R8 を使う |
|---|---|---|
| 原因切り分け | 難しくなる(最適化の影響が常に混入) | Release 限定なら影響範囲が明確 |
| 起動安定性 | 落ちやすくなることがある | keep ルール前提で安定化可能 |
| APK/AAB サイズ | 開発時はメリット小 | メリット大 |
今回のように Debug 実行の段階でクラッシュして開発が止まるなら、<AndroidLinkTool>r8</AndroidLinkTool> を無条件で書くのは避け、Release 条件付きに寄せるのが定番です。
推奨:R8 を Release ビルドだけ有効化する(安全な落としどころ)
「R8 を完全に捨てる」よりも、実運用ではRelease のみ有効化がバランス良いです。Debug はデバッグのしやすさと安定性、Release は最適化とサイズ削減、という役割分担にします。
csproj の考え方は次の通りです。
- Debug:
AndroidLinkToolを指定しない(=強い最適化を走らせない) - Release:
AndroidLinkToolをr8にして最適化。ただし必要なら keep ルールもセット
例(そのまま貼れる形に整えています。プロパティ名や既存設定に合わせて調整してください)。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios</TargetFrameworks>
<UseMaui>true</UseMaui>
</PropertyGroup>
<!-- Debug は安定優先:R8 を強制しない -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<!-- ここに AndroidLinkTool を書かない(または既存指定を外す) -->
</PropertyGroup>
<!-- Release のみ最適化:R8 を有効にする -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<AndroidLinkTool>r8</AndroidLinkTool>
</PropertyGroup>
</Project>
ポイントは、「常時指定」をやめることです。Debug 実行で R8 を走らせないだけで、今回のような実行時 ClassNotFoundException の巻き込まれがかなり減ります。
Release で再発するなら:R8/ProGuard の keep ルールで SplashScreen を守る
Release の最適化は続けたいが、Release でも同じクラッシュが起きる場合は、次の方向で対処します。
- R8/ProGuard が androidx.core.splashscreen を削らないように keep する
- Crashlytics など別ライブラリが要求する keep ルールも同時に維持する
まずは最小限の keep から始めると、保守しやすいです。例えば次のようなルールを用意します(ファイル名は例です)。
# SplashScreen (AndroidX Core SplashScreen)
-keep class androidx.core.splashscreen.** { *; }
そして、そのファイルを Release ビルド時に確実に参照されるように設定します。ここがズレると、警告(XA4304)につながりやすい部分です。
プロジェクトによって指定方法は多少差がありますが、考え方は同じで「設定ファイルが存在し、正しいパスで、Release のビルドに取り込まれている」状態を作ります。Visual Studio の UI を使うなら、該当ファイルのプロパティでビルドアクションを ProguardConfigurationにする運用が分かりやすいです。
もし csproj で明示する運用なら、Release 条件付きで設定ファイルを取り込むのが安全です(プロジェクトの既存記述に合わせてください)。
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<!-- ファイル名は例。実ファイルと一致させる -->
<ProguardConfiguration Include="proguard-rules.pro" />
</ItemGroup>
重要なのは、keep ルールの内容以上に、“そのファイルが本当にビルドに入っているか”です。入っていない場合、いくらルールを書いても効果が出ません。
XA4304 警告の正体:設定ファイル参照の不整合が多い
質問にある XA4304 は、ざっくり言うと「ProGuard/R8 関連の設定ファイルを参照しているのに、ファイルが存在しない・パスが違う・ビルドに取り込まれていない」などの参照不整合で出やすい警告です。
Crashlytics をインストール/アンインストールした後に出始めるのは、次のような“残骸”が残ることがあるためです。
- csproj には
ProguardConfigurationや関連項目が残っているが、実ファイルを削除してしまった - ファイルはあるが、フォルダ移動でパスが変わった
- ファイルはあるが、ビルドアクションが適切でなく取り込まれていない
対処はシンプルで、次の観点を一つずつ潰します。
| 確認ポイント | 具体例 | 直し方 |
|---|---|---|
| ファイルは存在するか | 参照先の .pro/.cfg がない | ファイルを追加するか、参照設定を削除 |
| パスが正しいか | Configs\proguard-rules.pro に移動したのに参照が旧パス | csproj 参照パスを実体に合わせる |
| ビルドに入っているか | ファイルはあるが取り込まれない | ビルドアクション/ItemGroup を見直す |
警告が出ている状態で最適化を進めると、今回のような「本来入るべきものが入っていない」事故が起きやすくなります。クラッシュ原因の切り分けを楽にするためにも、XA4304 は早めに解消するのがおすすめです。
net9.0-android で依存関係が荒れやすいときの考え方
質問文にもある通り、net9.0-android 構成だと、導入している NuGet の世代(Xamarin 時代のパッケージなど)によっては、依存関係やビルドタスクが噛み合わずに問題が表面化することがあります。ここで大事なのは「net9 が悪い」と断定することではなく、切り分けの軸として使うことです。
- net8.0-android で再現するか:再現しないなら、net9 側の差分(タスク/最適化条件/依存関係解決)を疑える
- 新規 MAUI プロジェクトで再現するか:プロジェクトの汚れ(残った設定・壊れた参照)を排除して比較できる
- Crashlytics の有無で再現するか:導入前後で csproj や ProGuard 参照がどう変わるか差分が見える
“原因が一つとは限らない”のがこういうクラッシュの難しさです。だからこそ、再現条件を最小化して「どの設定がトリガーなのか」を先に確定させると、直し方がブレません。
実務で効く切り分け手順:最短で原因を固定する
現場で再現するクラッシュは、闇雲に触ると時間が溶けます。次の順序で確認すると、今回のタイプはかなり早く原因を固定できます。
Debug で落ちるか、Release で落ちるかを分ける
- Debug で落ちる:無条件で R8 を走らせている可能性が高い。まずは
AndroidLinkToolの常時指定を疑う。 - Release だけ落ちる:Release でのみ有効になる縮小(R8/ProGuard)やリンクが原因になりやすい。keep ルールの不足、または設定ファイル未適用を疑う。
ビルド成果物の“汚れ”を除去する
Crashlytics の導入/削除後は、生成物やキャッシュの影響で「設定を直したのに直らない」ように見えることがあります。次の掃除は効果が出やすいです。
- bin/obj を削除してからビルド
- NuGet パッケージの再復元(必要ならキャッシュクリア)
- 新規プロジェクトで同じ NuGet を入れて比較(差分が最強のヒントになる)
依存関係を見える化する
「入っているはずの AndroidX が入っていない」「別バージョンが混ざっている」は、目視では気づきにくいです。CLI でトランジティブ依存まで確認すると、衝突が見つかることがあります。
dotnet list package --include-transitive
この一覧で、AndroidX 関連が想定外に古い/重複しているなどが見えたら、バージョン固定(中央管理)や不要パッケージの整理が効く場合があります。
落ちない構成を作る:設定の“型”を決めてしまう
最終的に安定するプロジェクトは、だいたい次の “型” に収束します。
- Debug は最小限の最適化(クラッシュしない・デバッグしやすい)
- Release は最適化 + keep ルール(サイズ削減と安定を両立)
- 警告(XA4304 等)は放置しない(設定不整合のサイン)
今回の結論に沿って言い切るなら、<AndroidLinkTool>r8</AndroidLinkTool> を無条件に入れ続ける運用はおすすめしません。落ちる状態を作りやすく、しかも原因が分かりにくいからです。消す(または Release 限定にする)のは妥当で、さらに Release の最適化を続けたい場合は keep ルールまでセットにして“仕組みとして再発しない形”に整えるのがベストです。
よくある質問
R8 を無効にするとストア配布で不利になりますか?
“必ず不利”ではありませんが、一般に Release の R8 は サイズ削減と最適化のメリットがあります。一方で、設定が不十分だと今回のように実行時クラッシュという最悪の形で表面化します。まずはRelease 限定 + keep ルールで「落ちない最適化」を作るのが現実的です。
Debug でも最適化を効かせたいのですが?
アプリサイズやパフォーマンスを Debug で検証したい事情もあります。ただし、Debug に R8 を混ぜると再現性の低い問題を呼び込むことがあります。検証目的なら、Debug 常用ではなく、検証用の構成(例:Staging/QA)を作ってそこで R8 を有効化する方が運用が安定します。
Crashlytics を入れ直したら直ることがありますか?
あります。ただし「入れ直しで直った」の正体は、依存関係が整理された、設定ファイル参照が復活した、ビルド生成物がリセットされた、などであることが多いです。再発防止のためには、入れ直しに頼るよりも、Release 限定の最適化設定とXA4304 の解消を先に固める方が安心です。
まとめ:今回の現象は「参照不足」より「最適化で消された」が本命
InstallSplashScreen()には Xamarin.AndroidX.Core.SplashScreen が必須- 参照があるのに ClassNotFoundException なら、R8/ProGuard の削除を強く疑う
<AndroidLinkTool>r8</AndroidLinkTool>を常時入れるより、Release 限定が安全- Release で再発するなら、keep ルールと設定ファイル参照(XA4304)を整える
- 依存関係が荒れているときは、net8 で再現確認と新規プロジェクト比較が効く

コメント