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 を指定済みの場合の結論
| 項目 | 要点 |
|---|---|
| 既に満たしている条件 | TargetFramework が net9.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 report と Policy 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 系 | 最新メジャーへ更新。長期未メンテのフォークは回避し、公式パッケージへ寄せる。 | ネイティブ依存があるため特に要確認。 |
| SQLite | SQLitePCLRaw.bundle_e_sqlite3 | 最新安定版へ更新。旧版で .so が 16KB 非対応なら置換または自前ビルド。 | e_sqlite3 は ABI ごとに .so を同梱。 |
| メディア | Xamarin.Plugin.Media | 置換推奨:Microsoft.Maui.Essentials の MediaPicker に移行。 | 2023 年以降更新が少なく将来的な対応が不透明。 |
| バーコード | ZXing(旧系)ほか | ZXing.Net.MAUI など MAUI ネイティブ対応の現行ライブラリへ。 | 古い Xamarin.Android ラッパーは避ける。 |
| Firebase/Play Services | Xamarin.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" >> $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 なら自動検出してくれます。
- 内部テストトラックに AAB をアップロード。
- Pre-launch report を確認し、問題が出たデバイスタイプ・ABI・スタックトレースを収集。
- Policy status で「App bundles must support 16‑KiB pages」関連の警告がないかを確認。
- 対象ライブラリを更新/置換/再ビルド後、再アップロードして収束を確認。
エミュレーター/実機でのチェックポイント
- 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_LOADの p_align を確認。 - 古いツールチェーンでビルドされた .so は、16KB デバイスでロードに失敗し得る。
移行の進め方:実務フロー(チェックリスト)
- 棚卸し: スクリプトで .so を含む NuGet/自作モジュールを一覧化。
- 優先度付け: コア機能に直結するもの(DB・描画・メディア)を最優先に更新。
- 置換/更新: 可能な限りマネージド置換(Essentials/MAUI 標準)へ。残りは最新安定版へ更新。
- 再ビルド: 自作ネイティブは NDK r28+ と AGP 8.5.1+ で再リンク。
- テスト: エミュレーター/実機で主要動線を確認。
- 検証: 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 基準 | 確認方法 |
|---|---|---|
| TargetFramework | net9.0-android35 | csproj を確認 |
| ABI | arm64-v8a;x86_64 のみ | csproj/出力 AAB の Supported ABIs を確認 |
| NuGet の .so 混入 | 要対応パッケージが列挙済み | スクリプトの出力を保存・レビュー |
| NDK バージョン | r28+ | CI ログ/sdkmanager --list |
| Gradle Plugin | 8.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 ページ)対応・即席手順
- TargetFramework を
net9.0-android35に統一。 - ABI を
arm64-v8a;x86_64に絞る(v7a を外す)。 - NuGet を棚卸しし、.so 含有パッケージを洗い出す。
- 該当パッケージを更新 or MAUI 標準 API に置換。残りは NDK r28+ で再ビルド。
- CI で
sdkmanagerにより NDK r28+ を明示導入。 - 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 の Logcat で
linker/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 ログ |
| QA | API 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。

コメント