Drop-in fix for Google Play's 16KB memory page size requirement — no AGP upgrade needed.
Starting in 2025, Google Play requires production apps to support 16KB memory page sizes (for Android 15+ devices). If your app bundles native .so libraries compiled with the standard 4KB page alignment, Google Play will reject your AAB with:
Your app does not support 16 KB memory page sizes.
The "official" fix is to upgrade to AGP 8.5+, which handles 16KB alignment natively through bundletool. But for many apps, upgrading AGP means:
- Upgrading Gradle, Kotlin, and the Android SDK
- Upgrading React Native (if applicable) and all native dependencies
- Fixing breaking changes across dozens of libraries
- Risk of destabilizing a working production app
If you're on an older AGP/React Native version and can't (or don't want to) upgrade, this repo provides post-build patching scripts that fix the alignment without touching your build toolchain.
The 16KB requirement has two parts:
-
ELF alignment — Each
.sofile's PT_LOAD segments must declarep_align >= 0x4000(16KB). Most pre-built libraries ship withp_align = 0x1000(4KB). -
ZIP/AAB alignment — The
.sofiles inside the AAB must be positioned at 16KB-aligned offsets so that Play Store's bundletool generates compliant split APKs.
This repo provides two approaches to fix both:
| Approach A (Recommended) | Approach B (Alternative) | |
|---|---|---|
| Method | ELF patching + BundleConfig.pb protobuf patching | ELF patching + zipalign post-processing |
| How it works | Patches ELF headers + tells bundletool to use 16KB alignment | Patches ELF headers + physically realigns ZIP entries |
| Re-signs | AAB only (jarsigner) | Both APK and AAB (apksigner + jarsigner) |
| Dependencies | Python 3 | Python 3 + zipalign + apksigner |
| Complexity | Lower | Higher |
| APK sideload | 4KB aligned (fine for testing) | 16KB aligned |
Copy the scripts/ folder to your project root (next to android/):
your-project/
android/
scripts/
patch_elf_16kb.py
patch_bundleconfig_16kb.py
...
Open android/app/build.gradle and paste the following outside the android {} block (before dependencies {}):
tasks.whenTaskAdded { task ->
// Hook 1: Patch ELF PT_LOAD alignment from 4KB to 16KB
if (task.name.startsWith('merge') && task.name.endsWith('NativeLibs')) {
task.doLast {
def raw = task.name.substring('merge'.length(), task.name.length() - 'NativeLibs'.length())
def variant = raw.substring(0, 1).toLowerCase() + raw.substring(1)
// AGP 8.x includes task name in path; AGP 4.x does not
def mergedDir = new File(buildDir, "intermediates/merged_native_libs/${variant}/${task.name}/out/lib")
if (!mergedDir.exists()) {
mergedDir = new File(buildDir, "intermediates/merged_native_libs/${variant}/out/lib")
}
if (mergedDir.exists()) {
println "16KB ELF patch: scanning ${mergedDir}"
exec {
commandLine 'python3',
"${rootProject.projectDir}/../scripts/patch_elf_16kb.py",
mergedDir.absolutePath
}
} else {
println "16KB ELF patch: directory not found, skipping"
}
}
}
// Hook 2: Patch AAB BundleConfig.pb + re-sign
if (task.name == 'bundleRelease') {
task.doLast {
def bundleDir = new File(buildDir, "outputs/bundle/release")
def aabFile = new File(bundleDir, "app-release.aab")
if (aabFile.exists()) {
def patchScript = "${rootProject.projectDir}/../scripts/patch_bundleconfig_16kb.py"
// Patch BundleConfig.pb
def patchedTmp = new File(aabFile.parent, "_patched_tmp.aab")
println "16KB AAB: patching BundleConfig.pb..."
exec { commandLine 'python3', patchScript, aabFile.absolutePath, patchedTmp.absolutePath }
aabFile.delete()
patchedTmp.renameTo(aabFile)
// Re-sign (adjust to match your signing config)
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
println "16KB AAB: re-signing with jarsigner..."
exec {
commandLine 'jarsigner',
'-keystore', file(MYAPP_RELEASE_STORE_FILE).absolutePath,
'-storepass', MYAPP_RELEASE_STORE_PASSWORD,
'-keypass', MYAPP_RELEASE_KEY_PASSWORD,
aabFile.absolutePath,
MYAPP_RELEASE_KEY_ALIAS
}
}
println "16KB AAB: done"
}
}
}
}See
examples/build.gradle.snippetfor the full version with signing config options, andexamples/build.gradle.zipalign.snippetfor Approach B.
Add android:extractNativeLibs="false" to the <application> tag:
<application
android:extractNativeLibs="false"
...>This ensures native libraries are loaded directly from the APK without extraction, which is required for the 16KB alignment to be effective at runtime.
If your app uses product flavors, the bundleRelease task name will be different. For example:
- No flavors:
bundleRelease - Flavor "production":
bundleProductionRelease
Update the if (task.name == '...') check to match your variant.
# Build your release AAB (and APK if needed)
./gradlew clean assembleRelease bundleReleaseCheck the build log for these lines:
16KB ELF patch: scanning /path/to/merged_native_libs/...
patched: libhermes.so
patched: libreactnative.so
patched: libjsi.so
...
16KB AAB: patching BundleConfig.pb...
patched BundleConfig.pb (+PAGE_ALIGNMENT_16K)
16KB AAB: re-signing with jarsigner...
16KB AAB: done
Upload the AAB to Google Play. The 16KB error should be gone.
The AAB must be re-signed after patching because modifying the ZIP contents invalidates the JAR signature. The Gradle snippet supports multiple signing patterns:
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_STORE_PASSWORD=****
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_KEY_PASSWORD=****RELEASE_STORE_FILE=../keystores/release.keystore
RELEASE_STORE_PASSWORD=****
RELEASE_KEY_ALIAS=my-alias
RELEASE_KEY_PASSWORD=****See the commented-out "Option B" in examples/build.gradle.snippet.
Tested and confirmed working on Google Play with:
| Project | React Native | AGP | Gradle | Target SDK | Result |
|---|---|---|---|---|---|
| App 1 | 0.59.9 | 4.1.0 | 6.5 | 35 | Passed |
| App 2 | 0.63.3 | 4.2.2 | 6.9 | 35 | Passed |
| App 3 | 0.76.5 | 8.6.0 | 8.10.2 | 35 | Passed |
Should work with any AGP version below 8.5 (where native 16KB support was added). If you're on AGP 8.5+, you don't need this — bundletool handles it natively.
The merged native libs path varies by AGP version. Run this to find yours:
find android/app/build/intermediates/merged_native_libs -type dCommon patterns:
- AGP 4.x:
intermediates/merged_native_libs/release/out/lib - AGP 8.x:
intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib
The snippet already tries both paths. If yours is different, update the mergedDir path in the Gradle hook.
This means the AAB's BundleConfig.pb has an unexpected structure. Possible causes:
- AGP 8.5+ already includes 16KB alignment — you may not need this fix
- The protobuf structure changed in a newer bundletool version
Make sure you're using the same keystore that was used for the original signing config in build.gradle. The re-sign step only needs to restore the JAR signature that was invalidated by the ZIP modification.
android:extractNativeLibs="false" requires Android 6.0 (API 23)+ for reliable behavior. If your minSdkVersion is below 23, you may need to set it conditionally or keep it as true and rely solely on the BundleConfig.pb patch for Play Store compliance.
Walks all .so files in the given directories and patches ELF PT_LOAD program header entries:
- Reads the ELF header to determine 32-bit vs 64-bit and endianness
- For each PT_LOAD segment, if
p_align < 0x4000, sets it to0x4000(16KB) - Safe to run on already-aligned files (no-op)
Modifies the BundleConfig.pb protobuf file inside an AAB:
- Opens the AAB as a ZIP archive
- Finds the
BundleConfig.pbentry - Locates the
UncompressNativeLibrariesmessage (field 2 ofOptimizations) - Adds
alignment = PAGE_ALIGNMENT_16K(enum value 2) to the message - Adjusts the enclosing message length field
- Writes the patched AAB
This is equivalent to what AGP 8.5+ does natively through its bundletool version.
- AGP 8.5+: bundletool handles 16KB alignment natively
- No native code: pure Java/Kotlin apps without
.sofiles aren't affected - Internal testing only: Google Play only enforces this for production track releases
MIT