Check an APK for Android's 16 KB Page Size Requirement
On this page
Short answer: 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.
Who this is for: Android developers shipping their own app (Kotlin, Java, Flutter, React Native, Unity or plain NDK) who got a 16 KB warning in Play Console, or want to know before they do.
What you will learn:
- What the requirement actually checks, and why a pure Kotlin app usually passes
- How to read the analyzer’s PLAY-02, PLAY-03 and PLAY-04 results
- How to cross-check with
zipalign -c -P 16andllvm-readelf - How to fix it with NDK r28, the r27 flags, AGP and
apksigner - What REA, the open-source project by morluto, can and cannot tell you here (version 6.4.0)
Only check builds you own or are authorized to examine. Release candidates of your own app are the intended target.
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 rules check, from the tool’s source (src/tools/apk/rules/rules.ts, src/tools/apk/elf.ts):
- 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.
Step 2: Cross-check with Google’s tools
Both conditions can be confirmed with the Android SDK and 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.
Where REA fits, and where it stops
REA 6.4.0 has no 16 KB page size check. I searched the published 6.4.0 package (dist/) for page-size and alignment logic, and the Android tools do not read ELF p_align or ZIP data offsets. Use the analyzer and zipalign for the alignment question. REA is useful around it:
rea inspect-artifact app-release.apk --jsoninventories every ZIP entry..sopaths are classified asdynamic-library(classifyArtifactPathindist/artifacts/inventory/ArtifactGraphConstruction.js), withdeclared_sizeandcompressed_size. Equal sizes suggest an entry is stored uncompressed. For ZIP entries the inventory does not record a data offset (byteOffset: nullindist/artifacts/ZipArtifactReader.js), so it cannot replacezipalign.rea inspect-android-package app-release.apk --jsonreturnspackage_name,min_sdk,target_sdk, permissions and the decoded manifest (schema indist/domain/android/androidAnalysis.js). The 16 KB rule applies from targetSdk 35 up, so this is how an agent confirms whether the app is in scope. It requires a caller-supplied JADX MCP JAR (REA_JADX_MCP_JAR); see the JADX MCP setup guide. Its result also sayssignature_verification: "not_performed".- To look inside a failing library, for example to find out which SDK built it, open it in Ghidra through REA as described in native binary analysis with REA and Ghidra.
FAQ
My app is pure Kotlin or Java. Do I need to do anything?
Only if a dependency ships native code. Run Step 0. No .so files means nothing to fix.
Do 32-bit libraries need 16 KB alignment?
No. The requirement concerns 64-bit devices and processes, and PLAY-02 only checks arm64-v8a and x86_64. You still need 64-bit versions of every 32-bit library (PLAY-04).
The analyzer shows “Unknown” for PLAY-02.
The program headers of at least one library could not be read, for example because the file is damaged or its header table is unusually large. Unknown is not a pass. Check that library with llvm-readelf -lW.
Can I check an AAB? Yes, for ELF alignment (PLAY-02) and 64-bit coverage (PLAY-04). ZIP alignment (PLAY-03) applies to APKs, which Play generates from your bundle. To check your own bundle locally, build a universal APK with bundletool, as shown in the AAB to APK guide, and check that.
Next steps
- Run the full pre-launch check, including targetSdk and signing: APK & AAB analyzer
- Find the code behind a finding with JADX and Apktool: Android APK Analysis with REA
- Inventory and extract every entry of an APK or AAB: AAB to APK and APK Unpack
- Reference: Support 16 KB page sizes, zipalign, REA’s DISCLAIMER.md