.NET MAUIアプリのAndroid 15[16KBページ]対応ガイド|API35/NDK r28/AGP 8.5で合格する実務チェックリスト

Android 15(API 35)と Google Play の「16KB ページサイズ必須」ポリシーに合わせて、.NET MAUI アプリをどう移行すべきか――結論だけ先に言えば、純粋な C#(マネージド)だけで構成された MAUI 9.0.40 のプロジェクトなら、基本はそのままでOKです。注意が必要なのは .so を含むネイティブライブラリと、自作の JNI/C/C++ コード。この記事では、影響範囲の見極めから、パッケージの置換・再ビルド、CI/CD での NDK 固定、Play Console での検証まで、実務で迷わないように手順と具体例をまとめます。

目次

Android 15/16KB ページ必須化の背景と影響範囲

Android 15 世代では、メモリページサイズ 16KB のデバイスが一般化します。アプリ本体(IL→JIT/AOT のマネージドコード)はこの制約の影響をほぼ受けません。一方で、ELF 形式のネイティブ共有ライブラリ(.so)は、16KB ページに合わせたアラインメント・リンカ設定・ツールチェーンでビルドされている必要があります。

  • 対象になるもの: NuGet に内包された .so、自作の jniLibs、C/C++/Rust 等でビルドしたネイティブモジュール、NDK ベースのプラグイン。
  • 対象外のもの: .dll のみで構成されたマネージドライブラリ、XAML/C# の UI ロジック、.NET ランタイムの標準アセンブリ(MAUI が供給)。
  • よくある該当パッケージ例: SkiaSharp 系、SQLitePCLRaw(e_sqlite3 バンドル)、Xamarin.Firebase/GooglePlayServices の旧系、メディア/バーコード/機械学習系のネイティブバインディング。

.NET MAUI 9.0.40 + API 35 を指定済みの場合の結論

項目要点
既に満たしている条件TargetFrameworknet9.0-android35 で、プロジェクト本体が 純粋な C#(マネージドコード) だけなら追加修正は不要。
影響を受ける可能性.so を含むネイティブライブラリ(例: SkiaSharp / SQLitePCL / Xamarin.Firebase / Xamarin.Plugin.Media / バーコードリーダー系など)。.dll のみのパッケージは問題なし。
ライブラリ判定手順1) NuGet を .nupkg → .zip にリネームして展開
2) lib/ 配下を確認し、.so が含まれるパッケージをリストアップ
アップデート/置換・各ライブラリのリリースノート/Issue で Android 15 / 16KB 対応有無を確認
・更新予定がない旧 Xamarin 系は MAUI 標準 API(例: MediaPicker)や別ライブラリへ置換
・やむなくフォークする場合は Android NDK r28 以降Gradle Plugin 8.5.1 以降で再ビルド
動作検証Android 15 エミュレーター(API 35)で起動・主要機能を確認(再現しにくい場合は後述の Pre-launch report を併用)
Play Console チェック内部テストトラックに AAB をアップロードし、Pre-launch reportPolicy Status を確認
よくある注意点armeabi-v7a は削除推奨(64bit へ集約)
・CI の NDK 固定を r28+ に更新(例: ndkVersion "28.0.0"
・16KB 化でビルドサイズ/速度への影響は限定的

まずは「.so を含むパッケージ」を機械的に洗い出す

手で .nupkg を開くのは大変なので、以下のようなスクリプトで一括抽出します。

Windows(PowerShell)例

$solution = "C:\repo\YourMauiApp.sln"
# ① 参照パッケージ一覧
dotnet list $solution package --include-transitive | Out-File -FilePath packages.txt -Encoding utf8

# ② ローカルの NuGet キャッシュから .nupkg パスを推定

$packages = Select-String -Path packages.txt -Pattern '^\s+>\s+(\S+)\s+(\d+.\d+.*)$' | %{
$id=$*.Matches.Groups[1].Value; $ver=$*.Matches.Groups[2].Value
Join-Path $env:USERPROFILE ".nuget\packages$id$ver"
} | Where-Object { Test-Path $_ }

# ③ .nupkg を展開して .so を検出

$result = @()
foreach($p in $packages){
$nupkg = Get-ChildItem $p -Filter "*.nupkg" -Recurse | Select-Object -First 1
if($nupkg){
$tmp = Join-Path $env:TEMP ([IO.Path]::GetFileNameWithoutExtension($nupkg.FullName))
if(Test-Path $tmp){ Remove-Item $tmp -Recurse -Force }
Expand-Archive $nupkg.FullName $tmp
$so = Get-ChildItem $tmp -Recurse -Include *.so
if($so){ $result += [PSCustomObject]@{ Package=(Split-Path $p -Leaf -Resolve); HasSO=$true; Path=$p } }
Remove-Item $tmp -Recurse -Force
}
}
$result | Sort-Object Package | Format-Table 

macOS/Linux(bash)例

SOLUTION=./YourMauiApp.sln
dotnet list "$SOLUTION" package --include-transitive > packages.txt

grep -E "^\s+> " packages.txt | awk '{print $2" "$3}' | while read id ver; do
base="$HOME/.nuget/packages/$id/$ver"
nupkg=$(find "$base" -name "*.nupkg" | head -n1)
[ -z "$nupkg" ] && continue
tmp=$(mktemp -d)
unzip -qq "$nupkg" -d "$tmp"
if find "$tmp" -name "*.so" | grep -q .; then
echo "$id@$ver contains .so  ($base)"
fi
rm -rf "$tmp"
done 

出力に .so を含むパッケージが出てきたら要注意です。最新安定版が「Android 15/16KB」対応か、後述の基準で確認・更新します。

典型パッケージの対処パターン

カテゴリ該当例推奨アクション備考
画像描画SkiaSharp 系最新メジャーへ更新。長期未メンテのフォークは回避し、公式パッケージへ寄せる。ネイティブ依存があるため特に要確認。
SQLiteSQLitePCLRaw.bundle_e_sqlite3最新安定版へ更新。旧版で .so が 16KB 非対応なら置換または自前ビルド。e_sqlite3 は ABI ごとに .so を同梱。
メディアXamarin.Plugin.Media置換推奨Microsoft.Maui.EssentialsMediaPicker に移行。2023 年以降更新が少なく将来的な対応が不透明。
バーコードZXing(旧系)ほかZXing.Net.MAUI など MAUI ネイティブ対応の現行ライブラリへ。古い Xamarin.Android ラッパーは避ける。
Firebase/Play ServicesXamarin.Firebase.* 旧系アクティブに更新されているパッケージへ移行。更新が止まっている場合は代替設計も検討。ネイティブ依存+大規模。検証を厳密に。

自作ネイティブ(C/C++/Rust 等)を 16KB 化で再ビルドする

自分で JNI 連携している場合は、Android NDK r28 以降のツールチェーンで再リンクします。通常は NDK r28+Gradle Plugin 8.5.1+ の組み合わせで、16KB ページに適合した ELF が得られます。

CMake 例(必要に応じて)

ツールチェーンが適切なら追加フラグは不要です。制御が必要な環境では、リンカに最大ページサイズの指定を渡すことがあります(環境により異なります)。

# 参考例:明示フラグが必要な場合のみ
if (ANDROID)
  add_library(mylib SHARED src/main/cpp/mylib.cpp)
  # -- 以下は環境やリンカにより名称が異なる場合があります --
  target_link_options(mylib PRIVATE "-Wl,-z,max-page-size=16384")
endif()

注: フラグ名称はビルド環境・リンカ(lld 等)により異なるため、基本は NDK r28+ のデフォルト動作に従い、過度な個別フラグ設定は避けます。

.NET MAUI 側のプロジェクト設定テンプレート

最低限、API 35 をターゲットし、64bit ABI に集約し、CI で NDK r28+ を確実に使うようにします。

csproj テンプレート

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0-android35</TargetFramework>
    <OutputType>Exe</OutputType>
    <Nullable>enable</Nullable>


<!-- 64bit のみ(推奨)。v7a は削除 -->
<RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
<AndroidSupportedAbis>arm64-v8a;x86_64</AndroidSupportedAbis>

<!-- AAB で配布 -->
<AndroidPackageFormat>aab</AndroidPackageFormat>

<!-- リリースビルドの最適化 -->
<PublishTrimmed>true</PublishTrimmed>
<InvariantGlobalization>true</InvariantGlobalization>









 

ABI 集約のメリット

  • APK/AAB のサイズ縮小(v7a を落とすことで重複 .so を減らす)。
  • 検証対象 ABI が減り、障害切り分けが容易。
  • 64bit 最適化の恩恵(パフォーマンス・セキュリティ)。

CI/CD(GitHub Actions)で NDK を r28+ に固定する

ローカルは更新したのに CI が古い NDK のまま……という事故を避けるため、明示的に NDK をインストール&固定します。

name: build-android

on:
push:
branches: [ main ]
pull_request:

jobs:
android:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4


  - name: Setup .NET
    uses: actions/setup-dotnet@v4
    with:
      dotnet-version: '9.0.x'

  - name: Install Android SDK components
    uses: android-actions/setup-android@v3

  - name: Install NDK r28+
    run: |
      sdkmanager --install "ndk;28.0.12916984" "platforms;android-35" "build-tools;35.0.0" "platform-tools"

  - name: Export ANDROID_NDK_ROOT
    run: echo "ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/28.0.12916984" &gt;&gt; $GITHUB_ENV

  - name: Restore workloads
    run: dotnet workload restore

  - name: Build AAB (Release)
    run: dotnet publish src/YourMauiApp/YourMauiApp.csproj -c Release -f net9.0-android35 -p:AndroidPackageFormat=aab


Play Console での検証(Pre-launch report とポリシー)

16KB 非対応の .so が混入していると、実機テストや一部デバイスでの起動時にクラッシュ/ロード失敗が発生します。ローカル環境で再現しづらい場合も、Play Console の Pre-launch report なら自動検出してくれます。

  1. 内部テストトラックに AAB をアップロード。
  2. Pre-launch report を確認し、問題が出たデバイスタイプ・ABI・スタックトレースを収集。
  3. Policy status で「App bundles must support 16‑KiB pages」関連の警告がないかを確認。
  4. 対象ライブラリを更新/置換/再ビルド後、再アップロードして収束を確認。

エミュレーター/実機でのチェックポイント

  • Android 15(API 35) のシステムイメージで基本機能(起動・ログイン・メディア・カメラ・DB など)をひと通り実行。
  • 該当機能(メディア撮影/バーコード読み取り/ネイティブ描画等)を重点的にテスト。
  • 再現しない場合でも、Pre-launch report をもって最終確認とするのが堅実。

トラブルシューティング:よくある症状と解決

症状原因の典型対処
特定デバイスのみで起動時クラッシュ同梱 .so が 16KB ページ非対応/ABI ミスマッチ当該パッケージを更新/置換。自作なら NDK r28+ で再ビルド。
v7a のみクラッシュ/x86_64 だけ動く古い v7a 向け .so が混在v7a を配布から外す(arm64-v8a & x86_64 の 64bit 集約)。
ビルドは通るが Pre-launch で指摘CI で古い NDK を使用CI の NDK を r28+ に固定し、クリーンビルド。
特定の画像描画/動画再生で不具合ネイティブデコーダ/描画エンジンが旧版SkiaSharp/FFmpeg ラッパー等の更新と、ABI の見直し。

置換の具体例:Xamarin.Plugin.Media → MediaPicker

カメラ/ギャラリーの取得を Xamarin.Plugin.Media から MediaPicker に切り替えるサンプルです。ネイティブ .so から解放されるため、16KB 対応の負債も減ります。

using Microsoft.Maui.Essentials;

public async Task PickPhotoAsync()
{
try
{
var result = await MediaPicker.PickPhotoAsync(new MediaPickerOptions
{
Title = "写真を選択"
});
return result;
}
catch (FeatureNotSupportedException) { /* デバイス非対応 */ }
catch (PermissionException) { /* 権限不足 */ }
catch (Exception) { /* キャンセル等 */ }
return null;
}

public async Task CapturePhotoAsync()
{
try
{
var result = await MediaPicker.CapturePhotoAsync();
return result;
}
catch (FeatureNotSupportedException) { }
catch (PermissionException) { }
catch (Exception) { }
return null;
} 

「危険な .so」をピンポイントに見抜く追加テクニック

より厳密に確認したい場合は、展開した .so に対して プログラムヘッダのアラインメント などを確認します(詳解は割愛)。再現性が低い場合は、最終的に Pre-launch report の検出結果を根拠に判断するのが実践的です。

  • NDK の readelf / llvm-readobj などで PT_LOADp_align を確認。
  • 古いツールチェーンでビルドされた .so は、16KB デバイスでロードに失敗し得る。

移行の進め方:実務フロー(チェックリスト)

  1. 棚卸し: スクリプトで .so を含む NuGet/自作モジュールを一覧化。
  2. 優先度付け: コア機能に直結するもの(DB・描画・メディア)を最優先に更新。
  3. 置換/更新: 可能な限りマネージド置換(Essentials/MAUI 標準)へ。残りは最新安定版へ更新。
  4. 再ビルド: 自作ネイティブは NDK r28+ と AGP 8.5.1+ で再リンク。
  5. テスト: エミュレーター/実機で主要動線を確認。
  6. 検証: Play Console の Pre-launch report と Policy で最終確認。

よくある質問(FAQ)

Q. MAUI 9.0.40 & API 35 でビルドしているが、他に設定は必要?

純マネージドで .so を一切含まないなら基本不要です。外部パッケージに .so が入っていないかだけ確認してください。

Q. armeabi-v7a を残すべき?

現行の配布ターゲットでは、arm64-v8a / x86_64 の 64bit 集約が推奨です。v7a を残すと .so が重複し、更新漏れの温床にもなります。

Q. 16KB 対応でアプリサイズやパフォーマンスは悪化する?

一般に影響は限定的です。ABI 集約やトリミング最適化で相殺可能です。

Q. テスト環境が整わない……どうする?

Pre-launch report を活用してください。16KB 非対応の .so があれば、高確率で検出・指摘されます。

発見しづらい落とし穴と回避策

  • トランジティブ依存の盲点: 直接参照は安全でも、間接参照の古いラッパーが .so を同梱しているケース。
    –include-transitive 付きで必ず棚卸し。
  • CI の SDK/NDK 差分: ローカル更新後に CI を放置し、Pre-launch でのみ発覚。
    sdkmanager で NDK を明示インストール&環境変数固定。
  • 古い Xamarin.* の取り残し: MAUI 化の過程で残存。
    → Essentials/MAUI 標準 API へ積極的に置換。

実践テンプレ:移行後のセルフチェック表

チェック項目OK 基準確認方法
TargetFrameworknet9.0-android35csproj を確認
ABIarm64-v8a;x86_64 のみcsproj/出力 AAB の Supported ABIs を確認
NuGet の .so 混入要対応パッケージが列挙済みスクリプトの出力を保存・レビュー
NDK バージョンr28+CI ログ/sdkmanager --list
Gradle Plugin8.5.1+ビルドログ(AGP バージョンの表示)
Pre-launch reportエラーなしPlay Console
ポリシー「16‑KiB pages」違反なしPolicy Status

まとめ:最短コースで「16KB 必須」対応を完了する

  • 純マネージドならそのまま合格。ただし依存 NuGet に .so が紛れていないか棚卸し。
  • ネイティブ依存は 更新/置換/再ビルドの三択。迷ったら MAUI 標準 API へ退避。
  • CI の NDK r28+ 固定と、ABI 64bit 集約で事故を減らす。
  • 最終判定は Pre-launch report と Policy。ここでクリーンなら、安心して(内部→公開)へ進めます。

付録:プロジェクトに貼る「一枚メモ」

Android 15(16KB ページ)対応・即席手順

  1. TargetFramework を net9.0-android35 に統一。
  2. ABI を arm64-v8a;x86_64 に絞る(v7a を外す)。
  3. NuGet を棚卸しし、.so 含有パッケージを洗い出す。
  4. 該当パッケージを更新 or MAUI 標準 API に置換。残りは NDK r28+ で再ビルド。
  5. CI で sdkmanager により NDK r28+ を明示導入。
  6. AAB を内部トラックへ提出し、Pre-launch report/Policy を確認。

補足情報

  • Xamarin.Plugin.Media は更新が止まっており Android 15 対応は不透明。置換推奨です。
  • テスト環境が用意できない場合でも、Google Play の Pre-launch report が 16KB 非対応の .so を自動検出します。
  • 詳細仕様や方針は、Google の 16KB ページ移行ガイド/Play ポリシー「App bundles must support 16‑KiB pages」を確認してください(本記事では外部リンクは割愛)。

.NET MAUI 9.0.40 からの移行をさらに盤石にする実務 Tips

ビルド成果物の健全性を確認する

  • アプリ起動前にクラッシュログを拾う: Android Studio の Logcatlinker/art ログをフィルタリング。
  • サイズ差を可視化: v7a を外した前後で AAB サイズを比較し、差分をチーム共有。

依存の更新方針を決める

  • メジャーアップデートは 1 つずつ: SkiaSharp と SQLite を同時に上げると切り分けが困難。One change at a time を徹底。
  • 代替案を常備: 主要機能(カメラ・DB)は、標準 API で代替できる設計に寄せておくと、ポリシー変化に強い。

社内ライブラリの再配布ルール

  • NDK バージョンのメタ情報(r28+)を README に明記。
  • ビルドログの保存(AGP/NDK/Clang のバージョン行)。
  • ABI ごとの配布確認(arm64-v8a / x86_64 のみ)。

バージョンピン留め例(ローカル環境)

# SDK Manager での導入例(macOS/Linux)
yes | sdkmanager "ndk;28.0.12916984" "platforms;android-35" "build-tools;35.0.0"
# 環境変数(シェルの初期化ファイル等)
export ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/28.0.12916984"

「落として良い」ものの判断基準

  • 実装が薄いだけのラッパー: 置換・廃止を優先(壊れても影響が限定的)。
  • 更新停止のネイティブ依存: 現行維持はリスク。将来のポリシー変化で再度止まる。
  • メディア・描画・DB: クラッシュ時のユーザー体験への打撃が大きく、優先度高。

エンジニアリング管理:開発・QA・リリースの役割分担

役割責務成果物
開発.so 棚卸し/置換/再ビルド/CI 設定棚卸しレポート、PR、CI ログ
QAAPI 35 の手動/自動テスト、エミュレーター+実機カバレッジテスト観点表、障害票
リリース管理内部トラック配信、Pre-launch/Policy 監視レポート、承認記録

このガイドで得られる実務的な効果

  • 16KB 必須化での「突発クラッシュ」や「リジェクト」を事前に回避。
  • ABI 集約により配布サイズ・テストコストを削減。
  • 今後の OS/ポリシー変化にも耐える「マネージド優先」設計へ移行。

結論

MAUI 9.0.40 + API 35 の純マネージド構成なら基本無修正でクリア。しかし、.so を含むネイティブ依存は必ず棚卸しして、更新・置換・再ビルド(NDK r28+)を実施してください。最後は Pre-launch report と Policy の二段構えで健全性を担保。これで、Android 15/16KB ページ必須化への移行は完了です。


参考:この記事の要点(コピー用)

  • 対象:.NET MAUI 9.0.40、Android 15(API 35)、16KB ページ必須。
  • 純マネージドは基本 OK。.so を含む依存が要対応。
  • 棚卸しスクリプトで .so 保有パッケージを機械抽出。
  • 旧 Xamarin 系は MAUI 標準 APIまたは活発な代替へ。
  • 自作ネイティブは NDK r28+AGP 8.5.1+ で再ビルド。
  • ABI は arm64-v8a / x86_64 に集約、v7a は外す。
  • 最終確認は Pre-launch report / Policy

この記事を書いた人

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

コメント

コメントする

目次