Skip to content

syafiyft/android-16kb-fix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Android 16KB Page Size Fix

Drop-in fix for Google Play's 16KB memory page size requirement — no AGP upgrade needed.

The Problem

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.

Why upgrading isn't always an option

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.

How It Works

The 16KB requirement has two parts:

  1. ELF alignment — Each .so file's PT_LOAD segments must declare p_align >= 0x4000 (16KB). Most pre-built libraries ship with p_align = 0x1000 (4KB).

  2. ZIP/AAB alignment — The .so files 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

Quick Start (Approach A — Recommended)

Step 1: Copy the scripts

Copy the scripts/ folder to your project root (next to android/):

your-project/
  android/
  scripts/
    patch_elf_16kb.py
    patch_bundleconfig_16kb.py
  ...

Step 2: Add Gradle hooks

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.snippet for the full version with signing config options, and examples/build.gradle.zipalign.snippet for Approach B.

Step 3: Update AndroidManifest.xml

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.

Step 4: Adjust the task name for flavors (if needed)

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.

Step 5: Build and verify

# Build your release AAB (and APK if needed)
./gradlew clean assembleRelease bundleRelease

Check 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.

Signing Configuration

The AAB must be re-signed after patching because modifying the ZIP contents invalidates the JAR signature. The Gradle snippet supports multiple signing patterns:

gradle.properties (most common for React Native)

MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_STORE_PASSWORD=****
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_KEY_PASSWORD=****

signing.properties (separate file)

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.

Compatibility

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.

Troubleshooting

"16KB ELF patch: directory not found, skipping"

The merged native libs path varies by AGP version. Run this to find yours:

find android/app/build/intermediates/merged_native_libs -type d

Common 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.

"WARNING: UncompressNativeLibraries pattern not found in BundleConfig.pb"

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

jarsigner fails or AAB is rejected after signing

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.

App crashes on older Android versions

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.

How the Scripts Work

patch_elf_16kb.py

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 to 0x4000 (16KB)
  • Safe to run on already-aligned files (no-op)

patch_bundleconfig_16kb.py

Modifies the BundleConfig.pb protobuf file inside an AAB:

  • Opens the AAB as a ZIP archive
  • Finds the BundleConfig.pb entry
  • Locates the UncompressNativeLibraries message (field 2 of Optimizations)
  • 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.

When You Don't Need This

  • AGP 8.5+: bundletool handles 16KB alignment natively
  • No native code: pure Java/Kotlin apps without .so files aren't affected
  • Internal testing only: Google Play only enforces this for production track releases

License

MIT

About

Drop-in fix for Google Play's 16KB memory page size requirement — no AGP upgrade needed.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages