Skip to content
All posts

The Android 16 KB page size migration: what actually breaks and how to check your APK

Google now requires Android apps to support 16 KB memory pages. Here's what that means in practice, why native libraries are the ones that break, and how to verify yours are safe.

MFKAPPS 5 min read

If your app ships any native code — your own JNI, or just a third-party SDK that bundles a .so file — there’s a good chance it’s carrying a landmine that only goes off on newer hardware. Android now supports 16 KB memory pages alongside the traditional 4 KB ones, and Google Play requires apps targeting recent Android versions to work correctly on both. Most Kotlin-only apps sail through this with zero changes. Apps with native dependencies often don’t, and the failure mode is ugly: not a lint warning, a crash on install or first launch, only on the devices that actually use 16 KB pages.

I went through this migration across the suite — most of it was a non-event, except for one dependency that wasn’t. Here’s what the check actually involves.

Why page size matters at all

A memory page is the smallest unit the OS manages memory in. Android has used 4 KB pages since the beginning; some newer chipsets default to 16 KB pages instead, because larger pages reduce the overhead of managing memory for RAM-heavy workloads — fewer page-table entries, fewer TLB misses, faster app launches on average. The OS and the Android Runtime handle this transparently for ordinary Kotlin and Java code. It’s invisible to ViewModel, Room, Compose — none of that cares what the underlying page size is.

Native code cares, because native libraries are ELF binaries, and ELF binaries have their loadable segments aligned to a fixed boundary at build time. A .so built with 4 KB-aligned segments works fine on a 4 KB-page device. Load that same file on a device running 16 KB pages, and the dynamic linker can fail to map it correctly — the app doesn’t start, or crashes the moment it touches that library.

What actually breaks

Not your Kotlin code. The list of actual risk is short and specific:

  • Your own NDK/JNI modules, if you build any.
  • Third-party AARs that bundle prebuilt native libraries — image processing, crash reporting, database engines, ML runtimes. Anything with a .so inside its AAR is a candidate.
  • Old toolchain output. A library built with an older NDK, before the toolchain defaulted to 16 KB-aligned segments, is the common failure case even in an SDK that’s otherwise actively maintained — the binary just hasn’t been rebuilt since the alignment default changed.

What doesn’t break: pure-Kotlin dependencies, anything shipped only as .jar or .aar without native code, and Google’s own first-party libraries in recent releases, which have already been rebuilt with the correct alignment.

Checking what’s actually in your APK

Before touching build config, find out if you’re exposed at all:

unzip -l app-release.apk | grep '\.so$'

If that returns nothing, you’re done — there’s no native code to align. If it returns entries, check each library’s segment alignment directly:

unzip -p app-release.apk lib/arm64-v8a/libsomething.so | \
  readelf -l - | grep LOAD

Look at the Align column on each LOAD segment. 0x4000 (16 KB) means that library is already aligned correctly. 0x1000 (4 KB) means it isn’t, and it’s the one you need to chase — either an updated version of the SDK it came from, or a rebuild if it’s your own code.

Android Studio’s APK Analyzer surfaces the same information without the command line: open the APK, drill into a .so file under lib/, and it flags 4 KB-only alignment directly.

The fix, in order of how much control you have

  1. Your own native code: rebuild with a current NDK. Recent toolchain versions default to 16 KB-aligned output, so this is often just a version bump in build.gradle.kts plus a clean rebuild — no source changes.
  2. A third-party SDK with an old .so: update the dependency. Nearly every actively maintained SDK has shipped a 16 KB-aligned build; the fix is a version bump, not a workaround.
  3. A dependency that hasn’t updated: this is the uncomfortable case. Your real options are an issue filed against the SDK, a fork-and-rebuild if it’s open source, or dropping the dependency. There’s no APK-level trick that re-aligns a .so you didn’t build.

The library that caught me was exactly case 2 — a database extension shipping prebuilt native binaries for the at-rest encryption behind Granyn’s transaction history. A minor version bump carried the aligned build; nothing in the integration code changed at all.

Testing on an actual 16 KB device

Alignment checks tell you what should happen; testing on real 16 KB pages tells you what does. The Android Emulator ships system images built specifically for this — look for a “16 KB Page Size” variant of a recent API level in the SDK Manager, create an AVD from it, and install your APK exactly as you would for a release. If anything using an unaligned library is going to crash, it crashes there, immediately and reproducibly, which is a much better place to find it than a support ticket from a device you don’t own.

The takeaway

For most Kotlin-first apps, this migration is invisible — no native code, no exposure, nothing to do. For anything that pulls in native dependencies, the fix is almost always a dependency version bump rather than a rewrite: find the unaligned .so with readelf, update whatever shipped it, rebuild, and verify on a 16 KB system image before it ships. The check takes ten minutes. Skipping it means finding out from a crash report instead.