Android 16KB Page Size Adaptation
Starting November 1, 2025, Google Play requires all new apps and updates to support 16KB page size devices. This post documents the pitfalls and lessons learned during this adaptation.
Background
Android 15 introduced the 16KB page size mode. Traditional Linux and Android use 4KB pages, while ARM architecture natively supports 4KB / 16KB / 64KB page sizes. Google’s core motivation for pushing 16KB is larger TLB coverage and fewer page table levels, which significantly reduces TLB misses in memory-intensive scenarios and improves overall performance. Devices like Pixel 8/8a have already enabled 16KB mode by default on Android 16.
On the surface, adaptation is just adding a -Wl,-z,max-page-size=16384 linker flag to SOs, but in practice it’s far from that simple. Our project APK contained 133 arm64 SOs from extremely diverse sources, ultimately involving coordinated changes across 9 base library repositories and 6 business submodules.
SO Source Analysis
An SO’s origin in a medium-to-large app typically falls into these categories: self-built NDK compilation output, third-party SDK AAR bundles, transitive dependencies (you might not even know they exist), and remote dynamic delivery (not in the APK but loaded at runtime).
The first step is building a complete inventory, grouping all SOs in the APK by dependency and tracing each to specific Maven coordinates and version numbers. This is the foundation for all subsequent work. We used Gradle’s dependency tree combined with unzip -l on the APK to compile the full list, mapping each SO to its specific dependency chain — this inventory proved invaluable for later troubleshooting.
p_align Declarations Are Unreliable
Android 16’s linker64 uses FixMinAlignFor16KiB() to calculate the real compatible page size from LOAD segment actual file offsets, not relying on the p_align declared in ELF headers.
During adaptation, we found some SDKs only declared p_align = 0x4000 in the ELF header while LOAD segment file offsets remained 4KB-aligned — this causes direct loading failure on linker64. When communicating with SDK vendors, explicitly state: changing p_align alone is insufficient; recompilation and relinking is required.
The only reliable verification is real-device loading:
adb push libxxx.so /data/local/tmp/adb shell /system/bin/linker64 /data/local/tmp/libxxx.soSuccess outputs load_bias=0x...; failure reports alignment (4096) is not a multiple of the page size (16384). readelf -l can assist verification, but linker64 real-device results are authoritative.
Categorized Handling
Incompatible SOs don’t have a one-size-fits-all fix; they need categorized handling.
Self-Built SOs with Source Code
The simplest case — add the linker flag during NDK compilation:
LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384
# CMakeLists.txttarget_link_options(your_lib PRIVATE -Wl,-z,max-page-size=16384)Note: if the SO statically links third-party libraries (like OpenSSL), all linked libraries must also be compiled with 16KB alignment. Missing any single static library makes the final SO incompatible. We hit this exact pitfall — when recompiling FFmpeg, we missed the OpenSSL build script, causing HTTPS video playback to fail. It took quite a while to trace it back to OpenSSL’s SO not being recompiled.
Another subtle issue is FFmpeg legacy version assembly compatibility. FFmpeg 3.4’s aarch64 NEON assembly (fft_neon.S, h264idct_neon.S, sbrdsp_neon.S, etc.) has PIC relocation incompatibilities that cause compilation failure in 16KB alignment mode. The short-term fix is --disable-asm to disable assembly optimization (hardware decoding is unaffected); long-term requires upgrading to FFmpeg 4.x+ to restore assembly acceleration.
Third-Party SDKs
Contact SDK vendors for 16KB-compatible versions. Most major SDKs had already released or were releasing compatible versions by 2025.
Remotely Delivered SOs
If an SO isn’t packaged in the APK but loaded at runtime through a remote delivery mechanism, exclude it in build.gradle:
android { packagingOptions { jniLibs { excludes += ['**/libxxx.so'] } }}These SOs rely on the remote delivery mechanism for version compatibility and aren’t constrained by APK packaging. But watch for consistency between excludes configuration and the remote delivery manifest — we discovered a case where an SO was excluded in excludes but the remote delivery config marked it for local loading, causing a runtime not-found error.
Deprecated SOs
The best fix is removal. During adaptation, we found multiple SOs with no code references or deprecated functionality. Deleting them directly solved the 16KB issue while reducing package size. For example, our project had several early security hardening components; after the server team confirmed the related feature fields were deprecated, we cleaned up the SOs along with their ProGuard rules and initialization code.
SDKs Without Near-Term Compatible Versions
The trickiest situation. If an SDK vendor can’t provide a 16KB version promptly, assess the impact scope of SO loading failure on the app, and add fallback logic where necessary. For instance, we implemented a degradation for video thumbnail extraction — when the native library fails to load, it falls back to MediaMetadataRetriever’s pure Java implementation, preventing the entire feature from becoming unavailable.
Removing SoLoader
Many projects using Facebook open-source libraries (Fresco, Litho, etc.) load SOs via SoLoader. SoLoader has compatibility issues under 16KB mode — when decompressing SOs to the app’s private directory, alignment requirements may not be met.
Our approach was to completely remove SoLoader: upgrade Fresco to 3.x (no longer requires SoLoader), replace all SoLoader.loadLibrary() with System.loadLibrary(), and remove SoLoader.init() initialization calls.
Watch for side effects after removal: if you previously relied on SoLoader’s automatic dependency resolution to load libc++_shared.so, ensure this SO is correctly packaged in the APK after removal. One of our project’s flavors crashed during debug builds for this reason — the STL shared runtime library wasn’t correctly packaged.
Fresco 1.x → 3.x
The Fresco major version upgrade was one of the highest-effort parts of this adaptation:
AnimationListenerinterface signature change: callback parameter changed fromAnimatedDrawable2toDrawableImagePipeline.getMainFileCache()removed; disk cache lookup now usesgetDiskCachesStoreSupplier()ImagePipelineNativeLoader.load()no longer needs manual invocationCircleProgressBarDrawableand other custom Drawables conflict with built-in versions; custom implementations must be removedDefaultLifecycleObservershim class must be removed in favor of the built-in AndroidX version
Recommended: global search for keywords like AnimatedDrawable2, getMainFileCache, SoLoader, ImagePipelineNativeLoader and fix each occurrence.
Reflection Restrictions
Android 16 tightens reflective access to system properties. If code or SDKs use SystemProperties.get("net.dns*") to retrieve DNS, it will fail on 16KB mode devices. Use public APIs instead:
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);Network network = cm.getActiveNetwork();LinkProperties props = cm.getLinkProperties(network);List<InetAddress> dnsServers = props.getDnsServers();Verification
Post-adaptation verification has two parts.
SO Compatibility Verification: Use linker64 to load every arm64 SO in the APK one by one, establishing a compatibility baseline. After handling all incompatible SOs per the categorized strategies above, run full verification again.
Functional Regression: Dual-device regression on both 16KB devices (Pixel 8/8a, Android 16) and regular devices. Focus coverage on SO-heavy feature areas — video playback/recording, live streaming with co-hosting, voice message recording/playback, face authentication, image loading/caching, data persistence (database reads/writes), crash collection, and hot updates. These scenarios all have native libraries working behind the scenes and are the most likely failure points.
Additionally, integrating SO alignment checks into CI is recommended to prevent future iterations from introducing incompatible SOs.
Bonus Cleanup
The 16KB adaptation was a forced opportunity to touch the entire SO dependency graph, so we also performed some housekeeping: built a complete SO dependency inventory (source, version, Maven coordinates, and full transitive chains all recorded), removed multiple deprecated security components and SDKs, unified transitive dependency versions with resolutionStrategy.force, and audited remote delivery configs to ensure consistency with excludes.
Overall, 16KB adaptation isn’t just a compiler flag change — it’s a deep audit of an app’s entire native dependency landscape. The core challenges are scattered SO sources, verification requiring real devices, different SOs needing different handling strategies, and API changes from the Fresco major version upgrade. It involves coordinated changes across multiple base library repositories and business modules, and releases need to proceed in dependency order. Start early to leave buffer time for SDK vendor upgrades and internal testing.