Check an APK for 16 KB Page Size Compatibility (Flutter, React Native, NDK)
On this page
An APK is 16 KB compatible when every 64-bit native library (lib/arm64-v8a/*.so, lib/x86_64/*.so) has all of its LOAD segments aligned to at least 16 KB (p_align >= 0x4000), and every uncompressed .so inside the APK starts at a ZIP offset that is a multiple of 16,384. If your APK has no lib/ directory at all, it ships no native code, and the requirement does not touch it.
You can check both conditions in about ten seconds with this site’s APK & AAB analyzer, which reads the file in your browser and never uploads it. Then confirm the result with Google’s own tools: zipalign and an ELF reader. This guide does both on four APKs I built for the purpose, so every output below is real.
What the 16 KB requirement means
Android has historically used 4 KB memory pages. Newer 64-bit devices can run kernels with 16 KB pages. When the dynamic linker loads a .so, it maps each LOAD segment at a page boundary, so a library linked for 4 KB pages (p_align = 0x1000) cannot be loaded correctly on a 16 KB kernel. The app then fails to start or crashes on first use of the library.
Google’s 16 KB page sizes guide sets out what Play requires: apps that target Android 15 or higher must support 16 KB pages on 64-bit devices, and the developer documentation states that from February 1, 2027, updates that do not comply cannot be published. Earlier Play announcements and forum threads mention other dates (November 2025 with extensions), which probably describe a warning phase. Check your app’s policy status in Play Console for the date that applies to you.
Two separate things must be right:
- ELF alignment. Set by the linker when the
.sois built. Only the library’s author can change it. - ZIP alignment. Set when the APK is packaged. Uncompressed native libraries are mapped straight out of the APK, so their data has to start on a 16 KB boundary inside the ZIP. Compressed libraries are extracted at install time, so their offset does not matter.
Step 0: Does your app even have native code?
List the native libraries first:
unzip -l app-release.apk | grep '\.so$'
No output means no native code and nothing to fix. Many “pure” Kotlin apps still have native code, though, because an SDK brings it in: image loading, crypto, analytics, a database or an ML runtime. Framework apps always have some. Typical file names, as covered in the APK unpacking guide:
- Flutter:
libflutter.soandlibapp.so - React Native:
libhermes.soorlibhermes_executor.soand the React Native libraries - Unity:
libunity.so, pluslibil2cpp.sofor IL2CPP builds
Only 64-bit ABIs (arm64-v8a, x86_64) count for the 16 KB rules.
The test APKs
To have known answers, I compiled one tiny JNI library (libhello.so, five lines of C) with two NDK versions and packaged it four ways with Android build-tools 36.0.0 (aapt2, zipalign, apksigner), using a throwaway release key:
| APK | Library built with | Packaging |
|---|---|---|
old.apk |
NDK r27 (27.1) | zipalign -p 4, signed with --alignment-preserved true |
unaligned.apk |
NDK r28 (28.2) | zipalign 4 (no page alignment), --alignment-preserved true |
new.apk |
NDK r28 (28.2) | zipalign -P 16 4, default apksigner |
only32.apk |
NDK r28, armeabi-v7a only |
zipalign -P 16 4 |
The manifest sets minSdkVersion 24, targetSdkVersion 36 and android:extractNativeLibs="false", as the Android Gradle Plugin does for modern builds, and the libraries are stored uncompressed.
Step 1: Check the APK in your browser
Open the APK & AAB analyzer, choose the Google Play scenario, and pick your .apk or .aab. The page reads the ZIP central directory and the first bytes of each .so inside a Web Worker. You can watch the Network tab to confirm nothing is sent.
The three rules that matter here, with the evidence the analyzer produced for my samples (asOf 2026-10-14):
old.apk, an r27 library with 4 KB ZIP alignment, fails both 16 KB rules:
PLAY-02 fail high lib/arm64-v8a/libhello.so: PT_LOAD p_align=0x1000 (< 0x4000)
PLAY-03 fail high lib/arm64-v8a/libhello.so: data offset 4096 (mod 16384 = 4096)
note: Effective from 2027-02-01 (reject); shown as high risk until then.
unaligned.apk, a correctly linked r28 library packaged without page alignment, passes PLAY-02 and fails PLAY-03:
PLAY-02 pass
PLAY-03 fail high lib/arm64-v8a/libhello.so: data offset 728 (mod 16384 = 728)
new.apk passes PLAY-02, PLAY-03 and PLAY-04.
only32.apk passes both 16 KB rules, because its only library is 32-bit (its p_align is still 0x1000), and fails a different rule:
PLAY-04 fail reject lib/armeabi-v7a/ present, lib/arm64-v8a/ missing
How to read the severity
The analyzer has a date selector. Before 2027-02-01, PLAY-02 and PLAY-03 are shown as high risk with the note above. Set the date to 2027-02-01 and the same findings become reject. I ran both dates on all four files and the evidence lines did not change, only the severity. PLAY-04 has no future date: Play already requires 64-bit libraries, so it is a reject today.
What the three rules check:
- PLAY-02 parses the ELF program headers of every 64-bit
.soand requires everyPT_LOADsegment to havep_align >= 0x4000. If the headers cannot be read, the result is Unknown, not a pass. - PLAY-03 applies only to APKs (Play builds the APKs from an AAB itself). For each 64-bit
.sostored uncompressed, it reads the local file header and requires the data offset to be a multiple of 16,384. If the manifest setsandroid:extractNativeLibs="true", the libraries are extracted at install time and the rule passes. - PLAY-04 flags a 32-bit ABI directory without its 64-bit counterpart, and any
.sowhose ELF class ore_machinedoes not match its directory.
For an AAB, PLAY-02 reads base/lib/<abi>/*.so. Manifest-based rules show Unknown for AABs in the current version, because AAB manifests are protobuf, not binary XML. To check ZIP alignment for your own bundle, build a universal APK with bundletool, as shown in the AAB to APK guide, and check that.
Step 2: Cross-check with Google’s tools
Both conditions can be confirmed with the Android SDK (zipalign) and the NDK.
ZIP alignment with zipalign
zipalign -c -P 16 -v 4 app-release.apk
-c checks without writing, -P 16 asks for 16 KB page alignment of uncompressed .so files, and 4 is the normal alignment for everything else. Real output from build-tools 36.0.0 on my samples (other entries trimmed):
Verifying alignment of old.apk (4)...
4096 lib/arm64-v8a/libhello.so (BAD - 4096)
Verification FAILED
Verifying alignment of unaligned.apk (4)...
728 lib/arm64-v8a/libhello.so (BAD - 728)
Verification FAILED
Verifying alignment of new.apk (4)...
16384 lib/arm64-v8a/libhello.so (OK)
Verification successful
The offsets match the analyzer’s evidence exactly.
ELF alignment with readelf or objdump
The NDK ships llvm-readelf and llvm-objdump under toolchains/llvm/prebuilt/<host>/bin/. Extract the library and read its program headers:
unzip -o app-release.apk 'lib/arm64-v8a/*' -d /tmp/apk-libs
llvm-readelf -lW /tmp/apk-libs/lib/arm64-v8a/libhello.so
The NDK r27 build:
LOAD 0x000000 0x0000000000000000 ... 0x0005b0 0x0005b0 R 0x1000
LOAD 0x0005b0 0x00000000000015b0 ... 0x0000d0 0x0000d0 R E 0x1000
LOAD 0x000680 0x0000000000002680 ... 0x0001b8 0x000980 RW 0x1000
The NDK r28 build of the same source:
LOAD 0x000000 0x0000000000000000 ... 0x0005b0 0x0005b0 R 0x4000
LOAD 0x0005b0 0x00000000000045b0 ... 0x0000d0 0x0000d0 R E 0x4000
LOAD 0x000680 0x0000000000008680 ... 0x0001b8 0x000980 RW 0x4000
The last column is p_align. llvm-objdump -p shows the same thing as align 2**12 for the r27 build and align 2**14 for the r28 build, and 2**14 is the value Google’s check_elf_alignment.sh script (linked from the page-sizes guide) looks for.
You will also see a GNU_RELRO line. For the r28 build it starts at 0x8680 with a size of 0x980, so it ends at 0x9000, which is not a multiple of 0x4000. The analyzer deliberately does not check RELRO end alignment: the AOSP script does not check it either, and correctly built r28 libraries like this one would fail it.
Step 3: Fix it
Rebuild your own native code
- NDK r28 or newer produces 16 KB aligned libraries by default. My r28 build above needed no flags.
- NDK r27 needs an opt-in. With CMake, pass
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON; with ndk-build, setAPP_SUPPORT_FLEXIBLE_PAGE_SIZES := trueinApplication.mk. In r27’s ownbuild/cmake/flags.cmakeandbuild/core/build-binary.mk, that setting adds-Wl,-z,max-page-size=16384forarm64-v8aandx86_64only. - Any other toolchain: link with
-Wl,-z,max-page-size=16384. Rebuilding the r27 sample with that one flag gave0x4000on everyLOADsegment. - Do not set these to false on r28. In r28 the same variables work the other way: setting them to false adds
-Wl,-z,max-page-size=4096and gives you 4 KB libraries again.
The flexible-page-size option also defines __BIONIC_NO_PAGE_SIZE_MACRO, which removes the PAGE_SIZE macro. If your C or C++ code hardcodes 4096 or uses PAGE_SIZE, change it to getpagesize() or sysconf(_SC_PAGESIZE). Alignment flags do not fix code that assumes 4 KB pages at runtime.
Fix the packaging
-
Use Android Gradle Plugin 8.5.1 or newer, which aligns uncompressed native libraries to 16 KB.
-
If you cannot upgrade, compress the libraries instead. The installer then extracts them, at the cost of a larger install:
android { packaging { jniLibs { useLegacyPackaging = true } } } -
Watch your signing step.
apksigner sign --helpin build-tools 36.0.0 lists--lib-page-alignmentwith a default of 16384, and--alignment-preserveddefaults to false. In my test, signing an APK whose library sat at offset 4096 moved it to 16384 unless I passed--alignment-preserved true. Older build-tools or custom signing pipelines may not do this, so runzipalign -c -P 16 -v 4on the final signed artifact, not the intermediate one.
Third-party libraries (Flutter, React Native, Unity, SDKs)
You cannot relink a prebuilt .so. The fix is a newer version from its vendor:
- Find which library failed from the analyzer’s evidence (
lib/arm64-v8a/libxyz.so). - Map the file to the framework or SDK it belongs to (see Step 0), then upgrade to a release whose notes mention 16 KB page size support. Flutter, React Native and Unity have all published such releases. Check their release notes for the version that fits your project rather than relying on a version number from a blog post.
- Rebuild, and run the check again. Upgrading one SDK sometimes leaves another one behind.
An unmaintained SDK with no 16 KB build has to be replaced. Patching alignment fields in someone else’s binary is not a fix: the linker has to lay out the segments.
To test on a real 16 KB environment, the page-sizes guide describes emulator system images with 16 KB pages. On a device or emulator, adb shell getconf PAGE_SIZE prints the page size in bytes.