スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Androidの16KBページサイズ対応でNDKを指定しても反映されない問題の対処

こんにちは。ForSCHOOL開発グループの@s-hamada93です。

Google Playストアでは2025/11/01から、16KBページサイズをサポートしていないアプリを審査に提出できなくなります。現在Flutterで開発している我々のスタディサプリ for SCHOOLアプリでも、Google Playストアで配信するためには16KBページサイズをサポートするため対応が必要となりました。

プロジェクトのNDKのバージョンを上げてビルドすれば解決するものと思っていましたが、我々のプロジェクトではうまく行きませんでした。今回は16KBページサイズ対応のためのNDK指定で実際に必要となったワークアラウンドな設定を紹介します。

16KBページサイズのサポート

16 KB ページサイズのサポート

Google Play の 16 KB 互換性要件 2025 年 11 月 1 日より、Google Play に提出され、Android 15 以降のデバイスを対象とするすべての新規アプリと既存のアプリのアップデートは、64 ビット デバイスで 16 KB のページサイズをサポートする必要があります。

「16KBページサイズのサポートとは何か」や、サポート状況の確認方法と対応方法についての詳細については割愛いたしますので、上記ページをご覧ください。

プロジェクトで使用しているAGPやNDKのバージョンによって必要な対応は異なります。本記事ではスタディサプリ for SCHOOLアプリで16KB対応のサポートをするべく、16 KB ページサイズのサポートの手引きに従って実際に対応した作業について記します。

スタディサプリ for SCHOOLアプリでの対応

スタディサプリ for SCHOOLアプリでは16KBページサイズをサポートできておらず、Google Playストアには対応が必要な旨の警告が表示されていました。以下のように順を追って対応を進めました。

  1. 16KB非対応のネイティブライブラリの特定
  2. 該当のネイティブライブラリを含んでいる依存ライブラリの更新
  3. AGPバージョンを更新
  4. NDKバージョンを更新

16KB非対応のネイティブライブラリの特定

check_elf_alignment.shを使用して対応の必要なライブラリを特定しました。

Android Studio付属のAPK Analyzerを使用することでも同じ内容を確認できます。また、Google Playストアで表示されている警告の詳細で非対応なライブラリのみを確認することもできます。

以下はスタディサプリ for SCHOOLアプリに対して実行した際の出力結果の抜粋です。 (左記のOK・NGおよび[output dir]の表記は筆者による編集です)

    $ ./check_elf_alignment.sh [apk file]

    ...

    === ELF alignment ===
    /[output dir]/lib/armeabi-v7a/libflutter.so: ALIGNED (2**16)
    /[output dir]/lib/armeabi-v7a/libapp.so: ALIGNED (2**14)
OK  /[output dir]/lib/armeabi-v7a/libwebcrypto.so: UNALIGNED (2**12)
OK  /[output dir]/lib/armeabi-v7a/libbarhopper_v3.so: UNALIGNED (2**12)
    /[output dir]/lib/armeabi-v7a/libdartjni.so: ALIGNED (2**14)
OK  /[output dir]/lib/armeabi-v7a/libsqlite3.so: UNALIGNED (2**12)
    /[output dir]/lib/armeabi-v7a/libdatastore_shared_counter.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libflutter.so: ALIGNED (2**16)
    /[output dir]/lib/arm64-v8a/libapp.so: ALIGNED (2**16)
NG  /[output dir]/lib/arm64-v8a/libwebcrypto.so: UNALIGNED (2**12)
    /[output dir]/lib/arm64-v8a/libbarhopper_v3.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libdartjni.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libsqlite3.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libdatastore_shared_counter.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libflutter.so: ALIGNED (2**16)
    /[output dir]/lib/x86_64/libapp.so: ALIGNED (2**16)
NG  /[output dir]/lib/x86_64/libwebcrypto.so: UNALIGNED (2**12)
    /[output dir]/lib/x86_64/libbarhopper_v3.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libdartjni.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libsqlite3.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libdatastore_shared_counter.so: ALIGNED (2**14)
    Found 5 unaligned libs (only arm64-v8a/x86_64 libs need to be aligned).

ALIGNEDは16KBページサイズに準拠しているもので、UNALIGNEDは準拠しておらず要対応なものです。出力末尾に(only arm64-v8a/x86_64 libs need to be aligned)とあるように、arm64-v8ax86_64に属するファイルにUNALIGNEDのものが含まれてはいけません。(armeabi-v7aについては16KBページサイズ対応を考慮する必要はなくUNALIGNEDが含まれていても構いません)

確認した結果からスタディサプリ for SCHOOLアプリで対応が必要となったネイティブライブラリは下記2ファイルでした。これはGoogle Playストアの警告で確認できる非対応ライブラリ一覧にも確かに一致します。

  • lib/arm64-v8a/libwebcrypto.so
  • lib/x86_64/libwebcrypto.so

該当のネイティブライブラリを含んでいる依存ライブラリを更新

確認された非対応ネイティブライブラリは、Dartライブラリの webcrypto に含まれている共有ライブラリでした。webcryptoの最新バージョンでネイティブライブラリに対して16KB対応が施されているなら、ライブラリのアップデートで16KB対応は解決できるかもしれません。

しかしスタディサプリ for SCHOOLアプリでは、対応作業時点でwebcryptoの最新のバージョンである5.8.0を既に使用しており、またライブラリ側でも16KBページサイズ対応がされていないことが分かりました。そのため、ビルドするアプリ側で16KBページサイズ対応を行う必要があります。

AGPバージョンを更新

共有ライブラリのパッケージングを更新するにはAGP 8.5.1以上と8.5.1未満で必要な手順が異なること、8.5.1以上の使用を推奨することが書かれています。

AGP をバージョン 8.5.1 以降にアップグレードし、非圧縮の共有ライブラリを使用することをおすすめします。

対応作業時点で既に8.9.1を使用していたため16KB対応のためのアップデートは必須ではありませんでしたが、この機に更新しました。

android/settings.gradle.kts

    plugins {
  -     id("com.android.application") version "8.9.1" apply false
  +     id("com.android.application") version "8.13.0" apply false
    }

NDKバージョンを更新

スタディサプリ for SCHOOLアプリでは、プロジェクトで使うNDKをFlutter SDKで使用しているNDKと揃える方針としていました。対応作業当時はFlutter 3.35.4を使用しており、使用されているNDKは27.0.12077973でした。

これをr28系である28.2.13676358に固定して更新しました。Javaバージョンも併せて更新しています。

android/app/build.gradle.kt

    android {
        ...

        compileSdk = 36
  -     // Flutter 3.35.4 => 27.0.12077973
  -     ndkVersion = flutter.ndkVersion 
  +     ndkVersion = "28.2.13676358"
    }

    compileOptions {
        isCoreLibraryDesugaringEnabled = true
  -     sourceCompatibility = JavaVersion.VERSION_11
  -     targetCompatibility = JavaVersion.VERSION_11
  +     sourceCompatibility = JavaVersion.VERSION_17
  +     targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
  -     jvmTarget = JavaVersion.VERSION_11.toString()
  +     jvmTarget = JavaVersion.VERSION_17.toString()
    }

トラブル:NDK指定しても16KB対応したコンパイルがされない

ここまでやった通り、NDKバージョンをr28系に引き上げてアプリをビルドすれば16KBページサイズ対応されたものが出来上がる、と思っていました。16 KB ページサイズのサポートの他に、いくつかの16KBページサイズ対応について解説されている記事も参考にしましたが、AGPとNDKのバージョンを十分新しくした状態でコンパイルすれば16KBページサイズ対応ができている様子でした。

おそらく、16KBページサイズ対応を行われた多くの方々においては、ここまでの作業で対応が完了していたことでしょう。しかし我々の場合実際にはビルド結果は変わらず、依然として

  • lib/arm64-v8a/libwebcrypto.so
  • lib/x86_64/libwebcrypto.so

の2つはUNALIGNEDなままでした。

原因調査

色々と試してみたところ、android/app/build.gradle.ktに加えたNDKに関わるGradleの設定値がコンパイル時に無視されているかのような挙動が見られました。

  • 既存成果物の影響を疑いcleanビルドやgradleのキャッシュ削除などを試したが効果なし
  • 元よりNDK r27系であるのでCMakeの引数を追加する対応も試したが効果なし
  • abiFiltersarm64-v8ax86_64を指定するも効果なし
  • loggerを仕込んでGradleを動かすと、Gradle上ではプロジェクトのNDKは28.2.13676358として認識されていることは確認できた
  • NDKが未インストールの環境でビルドした際に27.0.12077973しかダウンロード・インストールされなかったため実際には28.2.13676358が使われていない

各モジュールにもNDK指定を適用する

そこで、次のような設定をandroid/build.gradle.ktsに書き加え、アプリのプロジェクト側だけでなく依存するモジュールに対してもNDKを指定しました。

android/build.gradle.kts ※android/app/build.gradle.ktではないことに注意

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

...

subprojects {
    afterEvaluate {
        extensions.findByName("android")?.let { androidExt ->
            val android = androidExt as com.android.build.gradle.BaseExtension

            // 一部のFlutter用のプラグインでnamespaceが認識されない問題の回避
            if (android.namespace == null) {
                android.namespace = project.group.toString()
            }

            if (plugins.hasPlugin("com.android.application") ||
                plugins.hasPlugin("com.android.library")
            ) {
                // 各バージョン値は android/app/build.gradle.kt と揃える
                android.apply {
                    compileOptions {
                        sourceCompatibility = JavaVersion.VERSION_17    
                        targetCompatibility = JavaVersion.VERSION_17
                    }
                    compileSdkVersion(36)
                    ndkVersion = "28.2.13676358"
                }
                tasks.withType<KotlinJvmCompile>().configureEach {
                    compilerOptions {
                        jvmTarget.set(JvmTarget.JVM_17)
                    }
                }
            }
        }
    }
}

ビルド結果

    $ ./check_elf_alignment.sh [apk file]

    ...

    === ELF alignment ===
    /[output dir]/lib/armeabi-v7a/libflutter.so: ALIGNED (2**16)
OK  /[output dir]/lib/armeabi-v7a/libwebcrypto.so: UNALIGNED (2**12)
OK  /[output dir]/lib/armeabi-v7a/libbarhopper_v3.so: UNALIGNED (2**12)
    /[output dir]/lib/armeabi-v7a/libdartjni.so: ALIGNED (2**14)
OK  /[output dir]/lib/armeabi-v7a/libsqlite3.so: UNALIGNED (2**12)
    /[output dir]/lib/armeabi-v7a/libdatastore_shared_counter.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libflutter.so: ALIGNED (2**16)
    /[output dir]/lib/arm64-v8a/libVkLayer_khronos_validation.so: ALIGNED (2**16)
OK! /[output dir]/lib/arm64-v8a/libwebcrypto.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libbarhopper_v3.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libdartjni.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libsqlite3.so: ALIGNED (2**14)
    /[output dir]/lib/arm64-v8a/libdatastore_shared_counter.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libflutter.so: ALIGNED (2**16)
OK! /[output dir]/lib/x86_64/libwebcrypto.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libbarhopper_v3.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libdartjni.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libsqlite3.so: ALIGNED (2**14)
    /[output dir]/lib/x86_64/libdatastore_shared_counter.so: ALIGNED (2**14)
    Found 3 unaligned libs (only arm64-v8a/x86_64 libs need to be aligned).

libwebcrypto.so が2つともALIGNEDになりました!これで16KBページサイズ対応ができました! (armeabi-v7aにはUNALIGNEDのものが含まれていますが問題ありません)

解消できた要因の整理と考察

webcryptoの16KBページサイズ対応に関わるissue内で、プロジェクトのNDKをr28系にするだけで対応できた人とそうでない人のやり取りが見つかりました。プロジェクト構成の違いに起因している可能性は否定できません。

webcryptoのbuild.gradleのコードを覗いてみたところ、build.gradleの中ではNDKバージョンが指定されていないことが分かりました。単純にNDKバージョンが指定されていれば問題とならなかったかもしれません。

また、調査の中でNDKバージョンだけでなくjvmTarget.set(JvmTarget.JVM_17)も指定しなければALIGNEDになりませんでした。build.gradleではJavaバージョン1.8が指定されているため、このバージョンが古すぎた可能性もあります。

以上のように、いくつかプロジェクトのNDK指定だけで対応できなかった原因は推測できるのですが、現時点では特定には至っていません。

まとめ

NDKバージョンをr28系に上げて16KBページサイズ対応を試みました。

android/app/build.gradle.kt

    android {
        ...

        compileSdk = 36
  -     // Flutter 3.35.4 => 27.0.12077973
  -     ndkVersion = flutter.ndkVersion 
  +     ndkVersion = "28.2.13676358"
    }

    compileOptions {
        isCoreLibraryDesugaringEnabled = true
  -     sourceCompatibility = JavaVersion.VERSION_11
  -     targetCompatibility = JavaVersion.VERSION_11
  +     sourceCompatibility = JavaVersion.VERSION_17
  +     targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
  -     jvmTarget = JavaVersion.VERSION_11.toString()
  +     jvmTarget = JavaVersion.VERSION_17.toString()
    }

それでも16KBページサイズ対応した状態でビルドされなかったため、各モジュールに対してもNDKバージョンを含めたコンパイルの設定値を指定することで対応できました。

android/build.gradle.kts ※android/app/build.gradle.ktではないことに注意

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

...

subprojects {
    afterEvaluate {
        extensions.findByName("android")?.let { androidExt ->
            val android = androidExt as com.android.build.gradle.BaseExtension

            // 一部のFlutter用のプラグインでnamespaceが認識されない問題の回避
            if (android.namespace == null) {
                android.namespace = project.group.toString()
            }

            if (plugins.hasPlugin("com.android.application") ||
                plugins.hasPlugin("com.android.library")
            ) {
                // 各バージョン値は android/app/build.gradle.kt と揃える
                android.apply {
                    compileOptions {
                        sourceCompatibility = JavaVersion.VERSION_17    
                        targetCompatibility = JavaVersion.VERSION_17
                    }
                    compileSdkVersion(36)
                    ndkVersion = "28.2.13676358"
                }
                tasks.withType<KotlinJvmCompile>().configureEach {
                    compilerOptions {
                        jvmTarget.set(JvmTarget.JVM_17)
                    }
                }
            }
        }
    }
}

おわりに

16 KB ページサイズのサポートや、その他の16KBページサイズ対応に関する記事などを読んで「少し設定を変更してビルドし直すだけで対応できる」と見込んでいましたが、予想外の手順が必要となる事態となりました。

同じように、NDKバージョンを変えてビルドしてもアプリが16KBページサイズ対応されないという方の助けとなれば幸いです。