Android 16 KB 页面大小适配:在线检查 APK 的 .so 对齐
本页目录
结论先行: 一个 APK 要满足 16 KB 页面大小要求,需要同时做到两点:每个 64 位原生库(lib/arm64-v8a/*.so、lib/x86_64/*.so)的所有 LOAD 段都按至少 16 KB 对齐(p_align >= 0x4000);每个以不压缩方式存进 APK 的 .so,在 ZIP 里的数据起始偏移都是 16384 的整数倍。如果 APK 里根本没有 lib/ 目录,说明没有原生代码,这条要求跟你无关。
这两点都可以用本站的 APK/AAB 分析与上架体检 在浏览器里十秒钟查完,文件不会上传。查完再用 Google 自己的工具复核:zipalign 和 ELF 读取工具。本文用我专门构建的四个测试 APK 把两种方法都走了一遍,下面所有输出都是实际运行得到的。
适合谁读: 发布自家应用的 Android 开发者,不管用的是 Kotlin、Java、Flutter、React Native、Unity 还是直接写 NDK。已经在 Play Console 里看到 16 KB 警告,或者想提前排查的,都可以按这篇来做。
读完你会知道:
- 这条要求到底检查什么,为什么纯 Kotlin 应用通常没事
- 怎么看分析工具里 PLAY-02、PLAY-03、PLAY-04 三条结果
- 怎么用
zipalign -c -P 16和llvm-readelf交叉验证 - 怎么用 NDK r28、r27 的开关、AGP 和
apksigner修好它 - REA(morluto 的开源项目)6.4.0 在这件事上能做什么、不能做什么
只检查你自己的构建,或者你有权检查的构建。自家应用的 release 候选包就是本文设定的检查对象。
16 KB 页面大小是怎么回事
Android 过去一直使用 4 KB 内存页,新的 64 位设备可以跑 16 KB 页的内核。动态链接器加载 .so 时,要按页边界映射每个 LOAD 段。按 4 KB 页链接的库(p_align = 0x1000)在 16 KB 内核上没法正确加载,应用会启动失败,或者第一次调用这个库时崩溃。
Google 的 16 KB 页面大小指南 写明了 Play 的要求:targetSdk 为 Android 15 及以上的应用,必须在 64 位设备上支持 16 KB 页;开发者文档写的是从 2027 年 2 月 1 日起,不合规的更新将无法发布。更早的 Play 公告和一些论坛帖子还提到过别的日期(2025 年 11 月,可申请延期),那很可能指的是警告期。具体哪天对你生效,以 Play Console 里的政策状态为准。
要满足要求,有两件事必须同时做对:
- ELF 对齐。 由链接器在构建
.so时决定,只有库的作者能改。 - ZIP 对齐。 由打包 APK 的步骤决定。不压缩的原生库会直接从 APK 里映射到内存,所以它的数据必须从 ZIP 内的 16 KB 边界开始。压缩的库在安装时会被解压出来,偏移就无所谓了。
第 0 步:先看应用里有没有原生代码
先列出原生库:
unzip -l app-release.apk | grep '\.so$'
没有输出,就是没有原生代码,什么都不用改。不过很多“纯” Kotlin 应用其实也带着原生代码,是 SDK 带进来的,比如图片加载、加密、统计、数据库或者端侧 ML 运行时。跨平台框架的应用则一定有。常见的文件名(详见 APK 解包教程):
- Flutter:
libflutter.so和libapp.so - React Native:
libhermes.so或libhermes_executor.so,以及 React Native 自己的库 - Unity:
libunity.so,IL2CPP 构建还有libil2cpp.so
16 KB 规则只看 64 位 ABI(arm64-v8a、x86_64)。
测试用的 APK
为了有标准答案,我写了一个很小的 JNI 库(libhello.so,五行 C 代码),分别用两个 NDK 版本编译,再用 Android build-tools 36.0.0(aapt2、zipalign、apksigner)和一把临时生成的 release 密钥,打成四个包:
| APK | 库的编译工具 | 打包方式 |
|---|---|---|
old.apk |
NDK r27(27.1) | zipalign -p 4,签名时加 --alignment-preserved true |
unaligned.apk |
NDK r28(28.2) | zipalign 4(不做页对齐),--alignment-preserved true |
new.apk |
NDK r28(28.2) | zipalign -P 16 4,apksigner 用默认参数 |
only32.apk |
NDK r28,只有 armeabi-v7a |
zipalign -P 16 4 |
Manifest 里写了 minSdkVersion 24、targetSdkVersion 36 和 android:extractNativeLibs="false",和新版 Android Gradle Plugin 的默认产物一致;.so 都以不压缩方式存放。
第 1 步:在浏览器里检查
打开 APK/AAB 分析与上架体检,场景选 Google Play,然后选择你的 .apk 或 .aab。页面在 Web Worker 里读取 ZIP 中央目录和每个 .so 开头的几 KB。你可以开着 Network 面板,确认没有任何请求带走文件。
和 16 KB 相关的是三条规则。下面是工具对我这几个样本给出的证据(体检日期 2026-10-14)。
old.apk:r27 编译的库,ZIP 内只按 4 KB 对齐,两条 16 KB 规则都没过:
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:库是 r28 正确链接的,但打包时没做页对齐,所以 PLAY-02 通过,PLAY-03 不通过:
PLAY-02 pass
PLAY-03 fail high lib/arm64-v8a/libhello.so: data offset 728 (mod 16384 = 728)
new.apk:PLAY-02、PLAY-03、PLAY-04 全部通过。
only32.apk:两条 16 KB 规则都通过,因为它唯一的库是 32 位的(它的 p_align 仍然是 0x1000);但它没过另一条规则:
PLAY-04 fail reject lib/armeabi-v7a/ present, lib/arm64-v8a/ missing
严重级别怎么理解
工具里有一个日期选项。2027-02-01 之前,PLAY-02 和 PLAY-03 显示为高风险,并附上面那句说明;把日期改成 2027-02-01,同样的问题就变成拒审。我用两个日期把四个文件都跑了一遍,证据一字不差,变的只有级别。PLAY-04 没有未来生效日期:Play 现在就要求提供 64 位库,所以它今天就是拒审。
三条规则具体检查什么,可以在工具源码里看到(src/tools/apk/rules/rules.ts、src/tools/apk/elf.ts):
- PLAY-02 解析每个 64 位
.so的 ELF 程序头,要求每个PT_LOAD段都满足p_align >= 0x4000。程序头读不出来时,结果是未知,不算通过。 - PLAY-03 只针对 APK(AAB 由 Play 自己生成 APK)。对每个不压缩存放的 64 位
.so,读取本地文件头,要求数据起始偏移是 16384 的整数倍。如果 Manifest 写了android:extractNativeLibs="true",库会在安装时解压出来,这条规则直接通过。 - PLAY-04 检查是否只有 32 位 ABI 目录而缺少对应的 64 位目录,以及
.so的 ELF 位数或e_machine是否和所在目录不符。
对 AAB,PLAY-02 读的是 base/lib/<abi>/*.so。依赖 Manifest 的规则在当前版本里对 AAB 显示为未知,因为 AAB 的 Manifest 是 protobuf 格式,不是二进制 XML。
第 2 步:用 Google 的工具复核
不要只凭一个工具的结论就发版。两个条件都能用 Android SDK 和 NDK 自带的工具确认。
用 zipalign 查 ZIP 对齐
zipalign -c -P 16 -v 4 app-release.apk
-c 表示只检查不写文件,-P 16 要求不压缩的 .so 按 16 KB 页对齐,最后的 4 是其他条目的常规对齐。下面是 build-tools 36.0.0 对样本的实际输出(省略了其他条目):
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
偏移和分析工具给出的证据完全一致。
用 readelf 或 objdump 查 ELF 对齐
NDK 在 toolchains/llvm/prebuilt/<host>/bin/ 下自带 llvm-readelf 和 llvm-objdump。把库解出来,读它的程序头:
unzip -o app-release.apk 'lib/arm64-v8a/*' -d /tmp/apk-libs
llvm-readelf -lW /tmp/apk-libs/lib/arm64-v8a/libhello.so
NDK r27 的产物:
LOAD 0x000000 0x0000000000000000 ... 0x0005b0 0x0005b0 R 0x1000
LOAD 0x0005b0 0x00000000000015b0 ... 0x0000d0 0x0000d0 R E 0x1000
LOAD 0x000680 0x0000000000002680 ... 0x0001b8 0x000980 RW 0x1000
同一份源码用 NDK r28 编译:
LOAD 0x000000 0x0000000000000000 ... 0x0005b0 0x0005b0 R 0x4000
LOAD 0x0005b0 0x00000000000045b0 ... 0x0000d0 0x0000d0 R E 0x4000
LOAD 0x000680 0x0000000000008680 ... 0x0001b8 0x000980 RW 0x4000
最后一列就是 p_align。用 llvm-objdump -p 看,r27 的是 align 2**12,r28 的是 align 2**14;页面大小指南里链接的 check_elf_alignment.sh 脚本找的正是 2**14。
你还会看到一行 GNU_RELRO。r28 产物的 RELRO 从 0x8680 开始、大小 0x980,结束在 0x9000,不是 0x4000 的整数倍。分析工具有意不检查 RELRO 结尾是否对齐:AOSP 的脚本也不检查这一项,而且像这样正确构建的 r28 库也过不了这个公式。
第 3 步:修复
重新编译自己的原生代码
- NDK r28 及以上默认就产出 16 KB 对齐的库。上面的 r28 产物没有加任何参数。
- NDK r27 需要手动开启。CMake 传
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON;ndk-build 在Application.mk里写APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true。从 r27 自带的build/cmake/flags.cmake和build/core/build-binary.mk能看到,这个开关只对arm64-v8a和x86_64追加-Wl,-z,max-page-size=16384。 - 其他工具链:链接时加
-Wl,-z,max-page-size=16384。我给 r27 样本只加了这一个参数重新编译,所有LOAD段就都变成了0x4000。 - 不要在 r28 上把这两个变量设成 false。 到了 r28,它们的作用反过来了:设成 false 会追加
-Wl,-z,max-page-size=4096,库又变回 4 KB 对齐。
这个开关还会定义 __BIONIC_NO_PAGE_SIZE_MACRO,也就是去掉 PAGE_SIZE 宏。如果你的 C/C++ 代码里写死了 4096 或用了 PAGE_SIZE,要改成 getpagesize() 或 sysconf(_SC_PAGESIZE)。对齐参数修不了运行时假定页大小是 4 KB 的代码。
修打包
-
使用 Android Gradle Plugin 8.5.1 及以上,它会把不压缩的原生库按 16 KB 对齐。
-
暂时升级不了,就改成压缩存放,让安装器去解压,代价是安装体积变大:
android { packaging { jniLibs { useLegacyPackaging = true } } } -
注意签名这一步。 build-tools 36.0.0 的
apksigner sign --help里,--lib-page-alignment默认是 16384,--alignment-preserved默认是 false。我实测时,一个库原本在偏移 4096 的 APK,签名后被挪到了 16384,除非显式传--alignment-preserved true。旧版 build-tools 或者自定义的签名流程不一定会这样做,所以要对最终签好名的产物跑zipalign -c -P 16 -v 4,而不是检查中间产物。
第三方库(Flutter、React Native、Unity 和各种 SDK)
预编译的 .so 你没法重新链接,只能换供应商的新版本:
- 从分析工具的证据里找出是哪个库没过(
lib/arm64-v8a/libxyz.so)。 - 按第 0 步的文件名对应到所属框架或 SDK,升级到发行说明里明确写了支持 16 KB 页面大小的版本。Flutter、React Native 和 Unity 都发布过这样的版本。具体哪个版本适合你的项目,去看它们的发行说明,不要照搬博客里的版本号。
- 重新构建,再查一遍。升级了一个 SDK,另一个 SDK 可能还没跟上。
如果某个 SDK 没有 16 KB 版本、而且已经不再维护,只能替换掉。去改别人二进制里的对齐字段不算修复,段的布局必须由链接器重新排。
想在真实的 16 KB 环境里测试,页面大小指南介绍了 16 KB 页的模拟器系统镜像。在设备或模拟器上执行 adb shell getconf PAGE_SIZE,会打印以字节为单位的页大小。
REA 能做什么,做不到什么
REA 6.4.0 没有 16 KB 页面大小检查。我在 npm 上发布的 6.4.0 包(dist/)里搜过页大小和对齐相关的逻辑,Android 相关工具既不读 ELF 的 p_align,也不读 ZIP 数据偏移。对齐问题请用分析工具和 zipalign。REA 在周边环节有用:
rea inspect-artifact app-release.apk --json列出每一个 ZIP 条目。.so路径被归类为dynamic-library(见dist/artifacts/inventory/ArtifactGraphConstruction.js里的classifyArtifactPath),带declared_size和compressed_size,两者相等说明条目很可能没压缩。但对 ZIP 条目,清单不记录数据偏移(dist/artifacts/ZipArtifactReader.js里是byteOffset: null),所以替代不了zipalign。rea inspect-android-package app-release.apk --json返回package_name、min_sdk、target_sdk、权限和解码后的 Manifest(结构定义在dist/domain/android/androidAnalysis.js)。16 KB 规则从 targetSdk 35 起适用,AI agent 可以用它确认应用在不在范围内。它需要你自己提供 JADX MCP JAR(REA_JADX_MCP_JAR),配置方法见 JADX MCP 配置教程。结果里还会写明signature_verification: "not_performed"。- 想看某个不合格的库内部,比如判断它是哪个 SDK 编出来的,可以按 REA + Ghidra 原生二进制分析 用 Ghidra 打开它。
常见问题
我的应用是纯 Kotlin/Java 写的,需要改吗?
只有依赖带了原生代码才需要。先做第 0 步,没有 .so 就什么都不用改。
32 位库也要 16 KB 对齐吗?
不用。这条要求针对 64 位设备和进程,PLAY-02 也只查 arm64-v8a 和 x86_64。但每个 32 位库仍然要有 64 位版本(PLAY-04)。
PLAY-02 显示“未知”怎么办?
至少有一个库的程序头读不出来,比如文件损坏,或者程序头表异常大。未知不等于通过,用 llvm-readelf -lW 单独查那个库。
能检查 AAB 吗? 可以查 ELF 对齐(PLAY-02)和 64 位覆盖(PLAY-04)。ZIP 对齐(PLAY-03)针对的是 APK,由 Play 根据你的 bundle 生成。想在本地检查自己的 bundle,可以按 AAB 转 APK 教程 用 bundletool 生成一个 universal APK 再查。
下一步
- 做完整的上架体检,包括 targetSdk 和签名:APK/AAB 分析与上架体检
- 用 JADX 和 Apktool 找到问题背后的代码:用 REA 分析 Android APK
- 盘点并提取 APK/AAB 的每个条目:AAB 转 APK 与 APK 解包
- 参考资料:Support 16 KB page sizes、zipalign、REA 的 DISCLAIMER.md