Taming 4KB vs 16KB Page Sizes on Android: A Developer’s Learning Log
Taming 4KB vs 16KB Page Sizes on Android: A Developer’s Learning Log
Part of my ongoing AI-assisted dev notes. This post documents how I updated an Android SDK’s native libraries and packaging to run cleanly on both 4KB and 16KB page-size devices, plus the decision-making, dead-ends, and final checklist.
Context: What I’m Building (and Why It Broke)
I maintain an Android SDK that ships native code (C/C++ via the NDK). Things worked fine on most devices—until I started testing on devices/emulators with 16KB kernel page size (increasingly common on Android 15+). My checker script began reporting UNALIGNED for some 64-bit .so files, and a few builds had runtime load issues.
Symptoms I saw:
- The alignment script flagged UNALIGNED (212)** for certain 64-bit libraries.
- 32-bit libraries frequently showed 212 (4KB) alignment.
- Packaging notice about zip alignment requiring newer build-tools.
Goals:
- Make sure 64-bit ABIs (
arm64-v8a,x86_64) are ELF-aligned for 16KB pages. - Ensure the APK stores native libs uncompressed and is zip-aligned to 16KB.
- Keep everything backward-compatible with 4KB devices.
What I Learned / Architecture Decisions
- ABI filters aren’t page-size switches. They only control which CPU architectures we target. Page-size compatibility is about how we build and package the native libs.
- 64-bit libs need 16KB ELF alignment (>=
2**14). 32-bit libs can stay at 4KB (2**12). - NDK r28+ makes 16KB ELF alignment easier for 64-bit out of the box. On NDK r27, I must set linker flags.
- Packaging matters: Native libs should be uncompressed in the APK and zip-aligned to 16KB for proper mapping on 16KB devices.
- Backward compatibility: 16KB-aligned 64-bit libs are safe on 4KB devices; 16KB is a multiple of 4KB.
- Ownership detection: To fix third-party
.sofiles, I need to locate the responsible AAR from the Gradle cache or via Android Studio’s Analyze APK and update/vendor-request.
Key Concepts Explained
Page Size (Analogy First)
Imagine the OS hands out memory in boxes. A 4KB device has small boxes; a 16KB device has bigger boxes. If your package (a segment in your .so) expects the wrong box size or isn’t aligned to the box boundaries, it’s harder (or impossible) to place neatly. That’s where ELF alignment comes in.
- 4KB pages: 4096 bytes (
2**12). - 16KB pages: 16384 bytes (
2**14). - Alignment ensures segments line up on page boundaries for efficient memory mapping.
ABIs (Who Speaks Which Language)
ABI defines how compiled code speaks to the CPU. On Android:
armeabi-v7a(32-bit ARM) → 4KB alignment typical.arm64-v8a(64-bit ARM) → must support 16KB (>=2**14).x86(32-bit Intel) → 4KB typical.x86_64(64-bit Intel) → must support 16KB (>=2**14).
Rule of thumb: 64-bit ABIs need ELF p_align >= 16KB. 32-bit ABIs can remain at 4KB.
ELF Alignment vs Zip Alignment (Two Layers)
- ELF alignment: internal to the
.sofile—its segments’p_align(via linker flags or NDK defaults). - Zip alignment: how the APK lays out the file bytes on disk. For native libs, keep them uncompressed and zip-aligned to 16KB so the loader can mmap efficiently on 16KB devices.
jniLibs (Two Meanings)
- Project source:
src/main/jniLibs/<abi>/libsomething.so— where you can drop prebuilt.sofiles to include in an app or library. - Inside an AAR:
jni/<abi>/libsomething.so— native libraries shipped by a third-party library you depend on.
Code Snippets & Scenarios
Scenario A — NDK r28+ (Recommended)
Why: Simplest path. NDK r28 aligns 64-bit ELF segments appropriately by default.
No extra flags required for 16KB on 64-bit in many cases, but still verify with the checker.
// build.gradle (module)
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
// With NDK r28+, 64-bit ELF alignment is typically handled.
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
}
Scenario B — NDK r27 (Add Flags)
Why: If you’re on r27 (or want explicit control), enable flexible page sizes + 16KB max page size on the linker.
// build.gradle (module)
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=0x4000"
}
}
}
}
# CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
set(CMAKE_ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES ON)
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=0x4000")
add_library(yoursdk SHARED
native-lib.cpp
)
target_link_libraries(yoursdk
android
log
)
Scenario C — Packaging for Zip Alignment (APK)
Make sure native libs are uncompressed so they can be zip-aligned to 16KB. Modern AGP versions handle this better.
// build.gradle (module)
android {
packagingOptions {
jniLibs {
useLegacyPackaging = false // keep .so uncompressed for proper 16KB zip alignment
}
}
}
Then install newer build-tools to validate zip alignment:
sdkmanager "build-tools;35.0.0-rc3"
# Ensure $ANDROID_HOME/build-tools/35.0.0-rc3 is first on PATH
Scenario D — Finding Which AAR Owns a .so
If libAndroidSdk.so or libfcgdsxh.so isn’t in your repo, locate the vendor AAR:
CACHE="$HOME/.gradle/caches/modules-2/files-2.1"
# Who ships libAndroidSdk.so?
find "$CACHE" -type f -name "*.aar" -print0 \
| xargs -0 -I{} sh -c 'unzip -l "{}" | grep -q "libAndroidSdk.so" && echo "{}"'
# Who ships libfcgdsxh.so?
find "$CACHE" -type f -name "*.aar" -print0 \
| xargs -0 -I{} sh -c 'unzip -l "{}" | grep -q "libfcgdsxh.so" && echo "{}"'
From the path, you’ll see groupId/artifactId/version. Update that dependency to a 16KB-ready release or ask the vendor to rebuild with the flags above (or using NDK r28+).
Scenario E — Verifying Alignment
1) ELF (internal)
Expect ALIGNED (2**14) (or higher like 2**16) for 64-bit libs:
# Quick scan (custom script output)
./check_elf_alignment.sh app-arm64-v8a.apk | grep -E 'ALIGNED|UNALIGNED'
# Manual check p_align for a single .so
unzip -p app-debug.apk lib/arm64-v8a/libSomething.so > /tmp/libSomething.so
readelf -l /tmp/libSomething.so | grep -A1 'LOAD' # p_align should be 0x4000 for 64-bit
2) Zip (packaging)
After installing build-tools 35+, re-run the zip portion of your checker. Confirm .so files are stored (uncompressed) and 16KB zip-aligned inside the APK.
What Worked, What Didn’t
Worked:
- Upgrading to NDK r28+ simplified the process; ELF alignment for 64-bit was compliant by default.
- Setting
useLegacyPackaging = falseensured native libs were kept uncompressed for proper zip alignment. - The Gradle cache search reliably identified which third-party AARs contained the offending
.sofiles.
Didn’t (or was misleading):
- The checker reporting UNALIGNED for 32-bit ABIs initially caused alarm. It’s expected (4KB) and OK to ignore for 32-bit.
- Assuming ABI filters alone would solve page-size issues—they don’t. Build + packaging do the heavy lifting.
What I’d Do Differently
- Automate the checks in CI: Fail builds if any
arm64-v8a/x86_64.soshows< 2**14, or if zip alignment/packaging regress. - Vendor SLAs: Ask third-party providers to confirm 16KB readiness in release notes. Keep a constraint map in Gradle to force minimum versions.
- Early device coverage: Always test on at least one 4KB and one 16KB emulator/device before cutting a release.
References / Further Reading
- Android NDK docs (ELF, page sizes, packaging)
- Android Gradle Plugin release notes (native packaging and extractNativeLibs)
readelf,llvm-readobjtooling guides- Android Studio Analyze APK feature
- App Bundle / Play Console device catalog for ABI coverage
(Tip: for production docs, link to the exact Android Dev pages and your internal runbooks.)
Final Checklist (Copy/Paste for CI)
- Build: NDK r28+ (or r27 +
-Wl,-z,max-page-size=0x4000+ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES). - ELF alignment: All
arm64-v8a/x86_64libs reportALIGNED (2**14)or higher. - Zip alignment: Use build-tools 35+, ensure native libs are uncompressed and 16KB zip-aligned in the APK.
- 32-bit (
armeabi-v7a,x86):UNALIGNED (2**12)is OK. - 3rd-party libs: Updated to 16KB-ready versions; no unaligned 64-bit
.so. - Testing: Smoke test on one 4KB and one 16KB device/emulator.
If you found this useful or have a different setup, ping me—always curious to compare notes.