rea-reverse

Android 16 KB 页面大小适配:在线检查 APK 的 .so 对齐

基于 REA 6.4.0更新于 约 9 分钟读完
本页目录

结论先行: 一个 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 里的政策状态为准。

要满足要求,有两件事必须同时做对:

  1. ELF 对齐。 由链接器在构建 .so 时决定,只有库的作者能改。
  2. 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 再查。

下一步