上周三凌晨两点,我盯着 Pixel 9 的 logcat 发呆。一个上线两年多、DAU 不到五万的工具类 App,在 Android 15 上冷启动必崩,tombstone 里 fault addr 指向 libnative-lib.so 的第二个 LOAD 段。代码没动过,Gradle 没升级,唯一的变量是那台测试机跑在 16KB 页大小的内核上。adb shell getconf PAGE_SIZE 回车,屏幕上跳出 16384,不是我这十年看惯的 4096。
那晚之后我把手上七个 App 全扫了一遍,六个的 so 没对齐:三个是我们自己写的 .so、两个广告 SDK、一个加固壳。写这篇文章就是想把检测和修复的路径记下来,省得你们也凌晨两点对着 tombstone 发呆。
先把数字对清楚
传统 Android 设备的物理内存页是 4KB,也就是 4096 字节;Android 15 起,一部分设备(尤其是 8GB 内存往上的旗舰)把页大小切成了 16KB,即 16384 字节。落到 ELF 文件里,就是 LOAD 段的对齐要求从 2**12 变成 2**14。
时间点记牢:Google Play 的硬性规定是 2025 年 11 月 1 日起,所有新提交的应用和更新,只要 targetSdk 打到 Android 15 及以上,就必须支持 16KB 页大小。受影响的是 64 位 ABI——arm64-v8a 和 x86_64;armeabi-v7a 那套 32 位 so 不用管,32 位设备不会开 16KB 页。搞不清自己包里有几个 ABI 的,unzip -l app.apk | grep '\.so$' 先看一眼。
三条命令,十分钟判一个 APK 的死活
我给组里定的流程固定三步,第一条看设备,第二条看 APK 的 zip 对齐,第三条逐个 so 看 ELF 对齐。第三条最容易被跳过,但它才是真正决定加载成功与否的那条。
# 1. 设备页大小,期望 16384
adb shell getconf PAGE_SIZE
# 2. APK 内 so 的文件偏移对齐(-P 16 表示按 16KB 页对齐检查)
zipalign -c -P 16 -v 4 app-release.apk

# 3. 逐个 so 看 LOAD 段对齐
unzip -o app-release.apk 'lib/arm64-v8a/*.so' -d /tmp/so
for f in /tmp/so/lib/arm64-v8a/*.so; do
echo "== $f"
llvm-objdump -p "$f" | grep -A2 LOAD
done
输出里 align 2**14 是合格,align 2**12 就是没对齐,得回去重编。手边没有 llvm-objdump 的用 readelf -l libfoo.so | grep LOAD 也一样。AOSP 里还有个官方脚本 check_elf_alignment.sh,扔进一个解压后的 APK 目录能一次性把结果打出来,比手搓循环省事。
修复:优先升 NDK,其次改链接参数
最省事的路径是把 NDK 升到 r28,它默认就以 16KB 对齐链接,重新出包基本就过了。如果因为某些依赖锁死在 r27 或更早,就自己在链接参数里加 -Wl,-z,max-page-size=16384:CMake 项目塞进 target_link_options,ndk-build 项目加 LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384。
AGP 那边也别大意。我们当时卡在 8.3.2,包里的 so 明明编对了,zipalign -c -P 16 还是 FAILED,升到 8.5.1 之后才转绿。另外 android:extractNativeLibs 要留在 false(AGP 8.x 默认就是),useLegacyPackaging 也保持 false,让 so 不压缩地待在 APK 里直接 mmap 加载;一旦走了解压到 /data/app/.../lib/ 的老路子,前面做的对齐就白费。
改完记得在真机上验收。SDK Manager 里能下载带 16KB 页大小的 Android 15 系统镜像,x86_64 和 arm64 都有;部分 Pixel 机型在开发者选项里也有一个 16 KB page size 的开关,打开要重启,但不是每台机器都有。
真正的地狱是第三方 SDK
自己写的 so 好办,重编就行。难的是包里那些黑盒:加固壳、广告 SDK、某些直播和播放器 SDK,都是老 NDK 编出来的,有的还带 so 加密。我在一个包里见过 11 个第三方 .so,5 个没对齐,最老的那个编译时间戳是 2019 年。
这类只能走三条路:催厂商出新版本;升级或换掉那个 SDK;实在不行把它的功能用自己的实现替掉。第三条听着夸张,但一个两年的项目里,能砍掉的 SDK 往往比想象中多。顺带说一句,Play Console 的 App Bundle 详情页会直接标出哪些 so 没对齐,比你自己扫一圈快得多。
说点不一样的:这不是适配题,是环境迁移题
我的判断是,大部分团队拖到现在,不是懒,是这件事在流程里“不可见”。它不是 API 变更,lint 不报,单元测试全过,CI 一路绿,只有真机——还得是 16KB 内核的真机——才会炸。国内很多团队又不发 Google Play,于是“Google 的政策”看着跟自己毫无关系,直到某天国产旗舰也把页大小切成 16KB。这个时间点我不敢预测,但方向很清楚:内存做到 12GB、16GB 之后,4KB 页的 TLB miss 成本已经很难看了。
跨平台框架的情况顺带说一句。Flutter 的 engine 层官方早就在处理,我印象里 3.27 之后再没在纯 Flutter 包里见过没对齐的 so,但你项目里但凡有一个自己写的原生插件,还是得按上面的命令过一遍。Unity 靠官方补丁,近两年的 LTS 里确实有 16KB 的构建选项,记得手动打开并配上对应版本的 NDK。React Native 的 Hermes 和新架构 so 由官方出,相对可控,风险还是落在社区库上。这么排下来你会发现,框架选型在这件事上帮不了你,真正的变量是你引了多少原生代码。
收尾 checklist
- 用
adb shell getconf PAGE_SIZE确认测试机是不是 16384 zipalign -c -P 16 -v 4 your.apk,全部 OK 才算过- 扫
arm64-v8a和x86_64下所有 so 的 LOAD 对齐,看是不是2**14 - NDK 升 r28,或加
-Wl,-z,max-page-size=16384 - AGP 版本 ≥ 8.5.1
extractNativeLibs=false、useLegacyPackaging=false- 在 16KB 系统镜像或真机上跑一遍冷启动,以及相机、录音这类会走到 native 的路径
- 2025 年 11 月 1 日之前,把所有线上的包重新出一遍
折腾完这轮我最大的感受是:Android 生态里最难对付的从来不是新 API,而是这种不声不响改了默认值、又把截止日期写在后台角落里的东西。下次再遇到类似的,希望这套流程还能复用一半。