Android 15 适配之16K Page Size :为什么它会是最坑的一个适配点
首先什么是 Page Size ?一般意义上,页面(Page)指的就是 Linux 虚拟内存管理中使用的最小数据单位,页面大小(Page Size)就是虚拟地址空间中的页面大小, Linux 中进程的虚拟地址空间是由固定大小的页面组成。
Page Size
对于虚拟内存, CPU 的内存管理单元(MMU)会将虚拟地址转换为物理地址,所以虚拟内存最终也会映射到物理内存页面。
而为了实现虚拟内存到物理的映射,两个地址空间都会被划分为多个固定页面,而虚拟空间和物理空间中的页面需要大小相同,通常长度为 4K,为了区分虚拟页面和物理页面,后者一般会被称为页框(page frames )。
❝就是每个应用都有自己独特的虚拟地址空间,并且它永远也不要关心其他应用在做什么,占据了哪些真实地址,实际物理地址映射,是由 Linux 内核去管理和分配,虚拟内存也是为什么系统支持多应用同时运行的基础。
剩下的就很简单了,因为 Android 用的是 Linux 内核,所以在这部分逻辑一直以来都是遵循 Linux 的实现,只是 Android 由于「历史因素」限制,一直只支持 4 KB 内存页面大小,而现在为了优化系统内存性能,提高内存密集型工作负载的性能,Android 15 开始将采用 16KB 页面大小的要求。
那前面说了那么多「无用的知识」,核心还是 Android 15 要启动 16K Page Size 了,对于我们来说有什么影响?
先说理论上的正面影响:
系统面临内存压力时缩短应用启动时间:平均缩短 3.16%,某些应用的改进甚至高达 30% 降低应用启动时的耗电量:平均减少 4.56% 相机启动速度更快:热启动速度平均加快 4.48%,冷启动速度平均加快 6.60% 改善系统启动时间:平均改善1.5%(约0.8秒)
那么负面影响是什么?你带有 .so
的 android 项目,很可能需要重新编译带有全新的动态库(.so
) 才能正常运行对应功能,不然大概率会 crash 。
至于你的项目里有没有动态库,相信你应该很清楚,如果不清楚,直接把 apk 拖到 Android Studio ,看看 lib 下是否有 so 文件即可。
那为什么说用 Android 15 启动 16K Page Size 后,以往使用了 C/C++(native)代码的基本都会 crash 呢?这就是前面说到的 Linux 下 Page Size 的实现。
可能大家会觉得,为什么不能有兼容层,将 4k 页面大小转换为 16k 页面大小?
理论上这是可行的,比如你可以使用虚拟化技术让 4K 的应用跑在 16K 主机上,但是想要系统直接混搭 Page Size 支持,是一个比较困难的事情。
前面我们知道,虚拟内存和物理内存之间存在映射,那么如果 CPU 处于 16K 模式,那么其实所有内容都会处于 16K 模式,简单不严谨的理解,这会是一个 CPU 全局寄存器设置的概念。
❝所以对于所有空间页面而言,在任意时刻要么是 4K,要么是 16K ,页表结构在 4K 模式和 16K 模式下完全不同(每级页面、级数、屏蔽)。
这里扯一段额外的东西, Apple Silicon 的 MacOS 其实是有混合 16K/4K 支持,事实上 macOS 本身始终以 16K 页面运行,只有 Rosetta 模式下应用会以 4K 模式运行:
❝我大概记得好像是,在 macOS 上,Rosetta 模式用户空间和内核空间的页面大小可以不同,不同的用户空间程序可以使用不同的页面大小运行。
因为 Linux 不支持混合 Page Size,默认上 android 也不支持,除非 Android OS 层后续也做类似 Rosetta 等处理。
如何检测
前面讲了那么多废话,大家肯定还是很关心,如何知道我的 App 是否支持 16K Page Size 运行。
❝虽然带有陈年
.so
的很大概率不行,但是也许呢?
最简单且最实用的做法就是通过模拟器 VanillaIceCream 的 APIs Experimental 16k Page Size ARM 64 v8a System Image
进行测试。
根据自己的设备,如上图选择下载一个 VanillaIceCream 的 16k Page Size 的模拟器镜像,然后在 Other Images 选择下载的镜像创建模拟器。
注意,这里干货来咯,因为老板本 AS 在 LLDB 调试 16 KB 模拟器系统映像会存在问题,所以至少需要下载一个 Android Studio Koala Feature Drop | 2024.1.2 Canary 5 ,就是如下图所示这个,带 Drop 版本的才能成功运行 16K Page Size 模拟器。
❝什么是 Drop 版本?可以参考:https://juejin.cn/post/7379816515551723546
运行模拟器之后,通过 adb shell getconf PAGE_SIZE
可以获取到一个 16384
的值,说明系统现在已经是 16K 的模式。
那么,我把 GSYVideoPlayer 运行到 16K 模拟器后,不出所料 IJK 内核无法正常播放。
另外,官方提供了一个 16 KB ELF 对齐脚本来验证共享库的 ELF 段是否正确对齐,如下脚本针对 GSYVideoPlayer 里的动态库进行校验后,输出却是比较意外。
#!/bin/bash
# usage: alignment.sh path to search for *.so files
dir="$1"
RED="\e[31m"
GREEN="\e[32m"
ENDCOLOR="\e[0m"
matches="$(find $dir -name "*.so" -type f)"
IFS=$'\n'
for match in $matches; do
res="$(objdump -p ${match} | grep LOAD | awk '{ print $NF }' | head -1)"
if [[ $res =~ "2**14" ]] || [[ $res =~ "2**16" ]]; then
echo -e "${match}: ${GREEN}ALIGNED${ENDCOLOR} ($res)"
else
echo -e "${match}: ${RED}UNALIGNED${ENDCOLOR} ($res)"
fi
done
可以看到此时动态库是 ALIGNED 的,并且是 2**16
,也就是 armv68a
的动态库此时的 ELF 对齐。
另外,通过 sdk 的 build-tools/35.0.0
下的 zipalign
命令运行:
./zipalign -c -P 16 -v 4 /Users/guoshuyu/workspace/android/GSYVideoPlayer/app/build/outputs/apk/release/app-release.apk
可以看到此时的 apk 对齐也是没有问题的,所以此时的运行问题可能更多在于 C++ 代码里的 mmap
或者 sysconf
等代码存在问题,例如写死了 4096
等硬编码。
兼容
对于 LLDB 调试 16 KB 模拟器,需要 NDK r27 RC 1 的支持, 如果使用 r27 及以上的版本,那么只需要在 Application.mk
配置:
APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
如果是 Android NDK r26 及更低版本 ,则需要在 Android.mk
启用 16 KB ELF 对齐 :
LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384"
另外,构建支持 16 KB 设备的应用还需要 AGP 8.3 + 的支持,16 K 设备会要求附带未压缩共享库的应用将它们对齐到 16 KB 压缩对齐,为此升级到 Android Gradle 插件 (AGP) 版本 8.3+ 会是一个比较好的选择。
如果使用 AGP 版本 8.2或更低的版本,那么另一种选择是切换到使用压缩共享库
如果无法将 AGP 升级到 8.3 或更高版本,那么另一种选择是切换到使用压缩共享库:
android {
...
packagingOptions {
jniLibs {
useLegacyPackaging true
}
}
}
也就是什么?适配的基础是重新编译 .so
,重新编译的基础是:
NDK r27 推荐,NDK r26 及更低版本可以修改 max-page-size
,当然太低是真不行,反正目前测试我的 NDK r10e 是有问额····AGP 8.3 + 推荐,低版本可以用 useLegacyPackaging
当然,重新编译只是基础,代码里使用了例如 mmap
或者硬编码 4096 的地方,都需要修改适配。
那么问题来了,老旧 .so
连源码都没有的情况下,是不是没救了? 答案是肯定的,确实没救了,更糟心的是:谷歌计划明年将对 16k 设备的支持作为 Google Play 应用程序提交的必要条件。
❝本质上你的
.so
可能已经是 16K 对齐,但是还存在逻辑适配问题。
所以逃是逃不开了,好消息是目前这是一个实验性的阶段,另一个好消息,那就是 Flutter 本身已经支持了 16k ,在测试里 Flutter 3.22 是可以在 16K 模拟器上正常运行,这一定程度也算不幸中的万幸了。
最后
所以心凉了没?从目前看,基于 Linux 内核下的 16K Page Size 很大可能不支持 4K Page Size 的 C/C++ native 代码,所以可以遇见的情况有这么几种:
Android OS 最终落地了混合 Page Size 支持 重新编译和修改 .so
支持 16K
所以大家觉得会是怎么样的一个结果?