In 2025, Google Play started gradually enforcing new requirements for apps targeting Android 15+ (targetSdk 35). One of the most painful ones is compatibility with devices using 16 KB memory pages (16KB page size).
In practice, it often looks like this:
- the app builds fine,
- it works locally,
- but when you try to publish an update in Google Play, you get an error saying your app does not support 16KB page size,
- and the root cause is… native
.solibraries inside your APK/AAB.
This post shows how to quickly check your build before uploading to Play, locate the problematic .so files, and automate the check in Gradle.
What is 16KB page size and why does it break publishing?
Historically, most Android devices used 4 KB memory pages. However, some newer devices (especially on Android 15 and later) use 16 KB.
If your app includes native code (NDK, game engines, SDKs, ML, games, etc.), your APK/AAB will contain .so files.
And here is the key point:
Compatibility is not determined by “zipalign”, not by Gradle settings, and not by targetSdk itself — it is determined by how your ELF libraries were built.
.so files contain a segment alignment field (LOAD Align) that you can inspect using readelf.
If a library is built with Align = 0x1000 (4 KB), it may be incompatible with 16 KB page size and Google Play may block publishing.
Which ABIs actually matter?
The 16 KB page size requirement is primarily relevant for 64-bit ABIs:
arm64-v8ax86_64
32-bit ABIs (armeabi-v7a, x86) are usually not critical (real devices on those ABIs almost always use 4 KB pages). However, depending on the exact Play Console validation, warnings may still appear for 32-bit libraries.
In my own checker I ended up validating only 64-bit ABIs — that matches the real-world problem.
Why did it publish yesterday but fails today?
A very common scenario:
- your previous release is already published,
- you make a small bug fix,
- build an update,
- and suddenly Google Play starts complaining.
This happens because:
- Play gradually tightens validations,
- or you updated targetSdk / AGP,
- or your dependencies changed,
- or the new build simply pulled different
.sofiles.
This is especially common with:
- game engines (libGDX, Unity, Godot),
- ML/vision SDKs,
- ad SDKs,
- “ready-made” AAR/JARs that embed
.sofiles.
How to find which .so files are actually problematic
You need one of these tools:
Option A: readelf (Linux/macOS)
On Linux it is usually available out of the box. On macOS you can install it via brew.
Option B (universal): llvm-readelf from Android NDK
This is the most convenient option because:
- it works on Windows,
- it is stable,
- it is always available if Android NDK is installed.
What is considered a “valid” Align?
Google talks about compatibility with 16 KB.
That means valid alignment values are:
0x4000(16 KB) ✅0x8000(32 KB) ✅0x10000(64 KB) ✅
So the rule is simple:
Alignmust be at least 0x4000 and a multiple of 0x4000.
And values like:
0x1000(4 KB) ❌0x2000(8 KB) ❌
— are a real problem.
Quick APK/AAB check (Python utility)
I wrote a small script that:
- unpacks an APK or AAB
- finds all
.sofiles - checks
LOAD Alignviallvm-readelf - prints a list of problematic libraries
- exits with an error (so it can be used in CI)
Download the script
👉 Download link:
app_checker.py
Example usage
|
1 2 |
python app_checker.py app-release.apk |
Example output when something is wrong:
|
1 2 3 4 |
16KB page-size check failed: [bad align=0x1000] artifact=app-release.apk abi=arm64-v8a file=lib/arm64-v8a/libgdx.so [bad align=0x1000] artifact=app-release.apk abi=x86_64 file=lib/x86_64/libgdx-freetype.so |
What to do if you find 0x1000
If the issue is in your own .so (you build native code yourself), the usual fix is:
- update Android NDK to a recent version,
- rebuild your
.so.
If the issue is in a third-party library, your options are:
- update the SDK/engine to a version where the
.sofiles are already rebuilt for 16KB, - or replace the library,
- or (in rare cases) build the natives yourself.
Automatic Gradle check (before uploading to Google Play)
Manual checks are useful, but the best approach is to automate everything so the build fails immediately if incompatible .so files end up in your APK/AAB.
I added a Gradle task called check16kPageSize that:
- finds release APK/AAB artifacts,
- unpacks them,
- checks
.soviallvm-readelffrom Android NDK, - checks only
arm64-v8aandx86_64, - fails with a clear list of problematic files.
Requirements
To run the Gradle task you need:
- Android NDK installed
- and Gradle must be able to locate
llvm-readelf
Usually NDK is installed via Android Studio:
Tools → SDK Manager → SDK Tools → NDK (Side by side)
Gradle task check16kPageSize
Paste this code into android/build.gradle (or into the module that builds your APK/AAB).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
task check16kPageSize { group = "verification" description = "Checks all .so in release APK/AAB and verifies LOAD Align is 16KB-compatible for 64-bit ABIs using NDK llvm-readelf." doLast { def llvmReadelf = resolveLlvmReadelf() def outputsRoots = [file("$buildDir/outputs"), file("$projectDir/release")].findAll { it.exists() } def artifacts = files(outputsRoots.collect { root -> fileTree(root) { include "**/*.apk" include "**/*.aab" } }).files.findAll { it.isFile() }.sort { a, b -> a.absolutePath <=> b.absolutePath } artifacts = artifacts.findAll { it.name.toLowerCase().contains("release") || it.parentFile.name.toLowerCase().contains("release") } if (artifacts.isEmpty()) { def searched = [file("$buildDir/outputs"), file("$projectDir/release")]*.absolutePath.join(", ") throw new GradleException("No release APK/AAB found. Searched: ${searched}") } def errors = [] def tmpRoot = file("$buildDir/tmp/check16kPageSize") tmpRoot.mkdirs() // Check only 64-bit ABIs def allowedAbis = ["arm64-v8a", "x86_64"] as Set artifacts.each { artifact -> def unpackDir = new File(tmpRoot, artifact.name + "-" + artifact.lastModified()) if (unpackDir.exists()) unpackDir.deleteDir() unpackDir.mkdirs() copy { from zipTree(artifact) into unpackDir } def soFiles = fileTree(unpackDir) { include "**/*.so" }.files soFiles.each { soFile -> def relPath = soFile.absolutePath.substring(unpackDir.absolutePath.length() + 1).replace('\\', '/') def abiMatcher = (relPath =~ /(arm64-v8a|armeabi-v7a|x86_64|x86)/) def abi = abiMatcher.find() ? abiMatcher.group(1) : "unknown" if (!allowedAbis.contains(abi)) { return } def stdout = new ByteArrayOutputStream() def execResult = exec { commandLine llvmReadelf.absolutePath, "-lW", soFile.absolutePath standardOutput = stdout errorOutput = stdout ignoreExitValue = true } if (execResult.exitValue != 0) { errors << "[readelf failed] artifact=${artifact.name} abi=${abi} file=${relPath}" return } def output = stdout.toString("UTF-8") output.eachLine { line -> def load = (line =~ /^\s*LOAD\s+.*\s(0x[0-9a-fA-F]+)\s*$/) if (load.matches()) { def alignStr = load.group(1).toLowerCase() long alignVal = Long.parseLong(alignStr.substring(2), 16) // Valid if >= 0x4000 and multiple of 0x4000 if (alignVal < 0x4000L || (alignVal % 0x4000L) != 0L) { errors << "[bad align=${alignStr}] artifact=${artifact.name} abi=${abi} file=${relPath}" } } } } } if (!errors.isEmpty()) { throw new GradleException("16KB page-size check failed:\n" + errors.join("\n")) } logger.lifecycle("check16kPageSize: OK (${artifacts.size()} artifact(s) scanned)") } } tasks.matching { it.name in ["assembleRelease", "bundleRelease"] }.configureEach { finalizedBy check16kPageSize } |
How to run it
After building a release:
|
1 2 |
./gradlew assembleRelease |
or
|
1 2 |
./gradlew bundleRelease |
the check will run automatically.
You can also run it manually:
|
1 2 |
./gradlew check16kPageSize |
Why the problem is usually in third-party SDKs
The most annoying part:
- you can update Gradle, AGP, targetSdk
- but if your APK contains a third-party
.sobuilt withAlign = 0x1000, nothing will change.
This is why the 16KB issue often hits:
- libGDX (especially
libgdx.soandlibgdx-freetype.so) - older TensorFlow Lite / ML Kit versions
- ad SDKs
- any AAR/JAR with embedded natives
Summary
If your app contains .so files, it is worth doing this right now:
- ✅ run a local APK/AAB check
- ✅ add the check to Gradle/CI
- ✅ catch broken libraries before uploading to Play
Because once Play Console starts rejecting your upload, you are usually already close to a deadline — and changing SDKs at the last minute is painful.
Useful links
- Android documentation about page sizes:
https://developer.android.com/guide/practices/page-sizes - Android Developers Blog about the 16 KB transition:
https://android-developers.googleblog.com/
If this post helped you — feel free to leave a comment with which SDKs/engines you hit the 16KB issue with. I’ll keep a list of common offenders and solutions.