Google Play 要求 2025 年 11 月 1 日起,所有新应用及更新必须支持 16KB 页面设备。借着这次适配,记录下过程中踩过的坑和一些经验。
背景
Android 15 引入了 16KB 页面大小模式。传统 Linux 和 Android 使用 4KB 页面,而 ARM 架构天然支持 4KB / 16KB / 64KB 三种页面大小。Google 推动 16KB 的核心收益是更大的 TLB 覆盖范围和更少的页表层级,在内存密集型场景下可以显著减少 TLB miss,提升整体性能。Pixel 8/8a 等设备已在 Android 16 上默认启用 16KB 模式。
表面上看,适配就是给 SO 加个 -Wl,-z,max-page-size=16384 链接参数,但实际做下来远没这么简单。我们项目 APK 中有 133 个 arm64 SO,来源极为分散,最终涉及 9 个基础库仓库和 6 个业务子模块的联动修改。
SO 来源梳理
一个中大型 App 的 SO 来源通常可以分为这几类:自研 NDK 编译产出、第三方 SDK 的 AAR 内嵌、传递依赖引入(你甚至不知道它的存在)、远程动态下发(不在 APK 中,但运行时加载)。
第一步是建立完整清单,把 APK 中所有 SO 按依赖分组,逐一追溯到具体的 Maven 坐标和版本号。这是后续一切工作的基础。我们用 Gradle 的依赖树配合 unzip -l APK 的方式整理出了全量清单,每个 SO 对应到具体的依赖传递链——这个清单在后续排查问题时非常有用。
p_align 声明不可信
Android 16 的 linker64 通过 FixMinAlignFor16KiB() 从 LOAD 段的实际文件偏移计算真实兼容页面大小,不依赖 ELF header 中的 p_align 声明值。
实际适配中发现,部分 SDK 只在 ELF header 中声明了 p_align = 0x4000,但 LOAD 段的文件偏移仍按 4KB 对齐,这种情况在 linker64 上会直接加载失败。和 SDK 方沟通时需要明确告知:仅改 p_align 声明不够,必须重新编译链接。
唯一可靠的验证方式是真机加载:
adb push libxxx.so /data/local/tmp/adb shell /system/bin/linker64 /data/local/tmp/libxxx.so成功会输出 load_bias=0x...,失败会报 alignment (4096) is not a multiple of the page size (16384)。readelf -l 可以辅助验证,但以 linker64 真机结果为准。
分类处理
不兼容的 SO 没有统一的修复方式,需要根据情况分类处理。
有源码的自研 SO
最简单的情况,NDK 编译时添加链接参数即可:
LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384
# CMakeLists.txttarget_link_options(your_lib PRIVATE -Wl,-z,max-page-size=16384)注意:如果 SO 静态链接了第三方库(如 OpenSSL),所有被链接的库也必须用 16KB 对齐编译,遗漏任何一个静态库都会导致最终 SO 不兼容。我们就踩过这个坑——重编译 FFmpeg 时遗漏了 OpenSSL 的编译脚本,导致 HTTPS 视频无法播放,排查了好一会儿才定位到是 OpenSSL 的 SO 没有一起重编译。
另外一个比较隐蔽的问题是 FFmpeg 旧版本的汇编兼容性。FFmpeg 3.4 的 aarch64 NEON 汇编(fft_neon.S、h264idct_neon.S、sbrdsp_neon.S 等)存在 PIC 重定位不兼容的问题,在 16KB 对齐模式下编译会失败。短期方案是 --disable-asm 关闭汇编优化(硬件解码不受影响),长期需要升级到 FFmpeg 4.x+ 才能恢复汇编加速。
第三方 SDK
联系 SDK 方获取 16KB 兼容版本。大部分主流 SDK 在 2025 年已经或正在发布兼容版本。
远程下发的 SO
如果 SO 不打包在 APK 中,而是通过远程下发机制在运行时加载,可以在 build.gradle 中排除:
android { packagingOptions { jniLibs { excludes += ['**/libxxx.so'] } }}这些 SO 由远程下发机制保证版本兼容性,不受 APK 打包限制。但要注意 excludes 配置和远程下发清单的一致性,我们就发现过某个 SO 在 excludes 中排除了,但远程下发配置里标记的是本地加载,导致运行时找不到。
废弃 SO
最好的修复是移除。实际适配中发现了多个无代码引用或功能已废弃的 SO,直接删除既解决了 16KB 问题,又减小了包体积。比如我们项目中有几个早期接入的安全加固组件,服务端确认相关功能字段已经废弃后,连同 SO、混淆规则、初始化代码一起清理掉了。
短期无法获得兼容版本的 SDK
最棘手的情况。如果 SDK 方无法及时提供 16KB 版本,需要评估该 SO 加载失败对 App 的影响范围,必要时添加 fallback 逻辑。比如我们对视频缩略图获取做了降级处理——当 native 库加载失败时,fallback 到 MediaMetadataRetriever 的纯 Java 实现,避免整个功能不可用。
移除 SoLoader
许多使用 Facebook 开源库(Fresco、Litho 等)的项目通过 SoLoader 加载 SO。SoLoader 在 16KB 模式下存在兼容性问题——它解压 SO 到应用私有目录时可能不满足对齐要求。
我们的方案是彻底移除 SoLoader:升级 Fresco 到 3.x(不再强制依赖 SoLoader),所有 SoLoader.loadLibrary() 替换为 System.loadLibrary(),移除 SoLoader.init() 初始化调用。
注意移除后的副作用:如果之前依赖 SoLoader 的自动依赖解析来加载 libc++_shared.so,移除后需要确保该 SO 被正确打包到 APK 中。我们项目的某个 flavor 就因为这个问题在 debug 构建时崩溃,排查发现是 STL 共享运行库没有被正确打包进去。
Fresco 1.x → 3.x
Fresco 大版本升级是这次适配中工作量最大的部分之一:
AnimationListener接口签名变更:回调参数从AnimatedDrawable2改为DrawableImagePipeline.getMainFileCache()被移除,磁盘缓存查找需改用getDiskCachesStoreSupplier()ImagePipelineNativeLoader.load()不再需要手动调用CircleProgressBarDrawable等自定义 Drawable 与内置版本冲突,需移除自定义实现DefaultLifecycleObservershim 类需移除,改用 AndroidX 内置版本
建议全局搜索 AnimatedDrawable2、getMainFileCache、SoLoader、ImagePipelineNativeLoader 等关键词逐一修复。
反射限制
Android 16 收紧了对系统属性的反射访问。如果代码或 SDK 中有通过 SystemProperties.get("net.dns*") 获取 DNS 的逻辑,在 16KB 模式设备上会直接失败,改用公开 API:
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);Network network = cm.getActiveNetwork();LinkProperties props = cm.getLinkProperties(network);List<InetAddress> dnsServers = props.getDnsServers();验证
适配完成后的验证分两部分。
SO 兼容性验证:用 linker64 逐一加载 APK 中所有 arm64 SO,建立兼容性基线。按上述分类策略逐一处理不兼容 SO 后,再次全量验证。
功能回归:在 16KB 设备(Pixel 8/8a, Android 16)和普通设备上双端回归。重点覆盖 SO 密集的功能区域——视频播放/录制、直播推流与连麦、语音消息录制与播放、人脸认证、图片加载与缓存、数据持久化(数据库读写)、崩溃采集与热更新。这些场景背后都有 native 库在工作,是最容易出问题的地方。
另外建议在 CI 中集成 SO 对齐检查,防止后续迭代引入不兼容的 SO。
顺带做的事
16KB 适配是一次被迫触及全量 SO 依赖的机会,借此我们也做了一些清理:建立了完整的 SO 依赖清单(来源、版本、Maven 坐标、传递链全部记录)、移除了多个功能已废弃的安全组件和 SDK、用 resolutionStrategy.force 统一了传递依赖的版本、审查了远程下发配置确保 excludes 和下发清单一致。
整体来说,16KB 适配不只是一个编译参数的修改,而是一次对 App 全量 native 依赖的深度梳理。核心难点在于 SO 来源分散、验证依赖真机、不同 SO 需要不同处理策略、以及 Fresco 大版本升级带来的接口变更。涉及多个基础库仓库和业务模块的协同修改,发版也需要按依赖顺序分步推进。建议尽早启动,为 SDK 方升级和内部测试留出缓冲时间。