在此之前,我们通过 《Android 15 上 16K Page Size 为什么是最坑》 介绍了:
- 什么是16K Page Size
- 为什么它对于 Android 很坑
- 如何测试
如果你还没了解,建议先去了解下前文,然后本篇主要是提供适配的思路,因为这类适配更多工作量在于实际的执行调整和编译跟踪,而非功能上的适配,本质不复杂,但可能量大且繁琐。
是的,想要适配你首选需要有 C/C++ 等 so 库的源码。
适配的开始,我们可以先粗暴的全局搜索 「4096」关键字,看看是否有将 4096 搭配 mmap
、sysconf
等相关的地方,因为 Android 上的 4K 这么多年已经「深入人心」,可以说不少代码都将 Android 默认为「4096」。
对于这些地方,我们可以通过类似 getpagesize()
等方式来进行调整,例如通过 ALOGV("####### %d", getpagesize());
可以看到在 Android15 上输出的是 16384 。
接着我们可以运行项目到 Android 15 的模拟器上(如果运行不起来看前文),看 so 是否能正常被加载执行,一般情况下你可能会看到类似:
java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH ···· (new hash type from the future?)
这类问题基本都是 so 文件没有 16K 编译对齐的原因,此时,根据你的 CMakeList 版本,可以使用 target_link_options
或者 target_link_libraries
进行调整。
例如 3.13 之前:
target_link_libraries(a4ijkplayer "-Wl,-z,max-page-size=16384")
例如 3.13 之后:
target_link_options(a4ijkplayer PRIVATE "-Wl,-z,max-page-size=16384")
注意 CMakeList 的版本还需要 SDK Manager 里有下载安装。
如果是 Android.mk
, 则添加 LOCAL_LDFLAGS 也可以配置 16K Page :
LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384
编译后,我们通过 readelf 工具,可以对比编译前后两个 so 的 elf 对齐情况,工具一般位于 /Users/guoshuyu/Library/Android/sdk/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin
,通过以下命令可以输出对应参数:
./aarch64-linux-android-readelf -l /Users/guoshuyu/workspace/android/******/libs/arm64-v8a/libijkffmpeg.so
如下两种图所示:
- 图 1 LOAD 段在 Align 栏目显示 1000 (16进制,即 4096) ,也就是还没增加 16K 对齐的状态
- 图 2 LOAD 段在 Align 栏目显示 4000 (16进制,即 16384) ,也就是修改编译后,已经增加 16K 对齐的状态
这里我们只关心 LOAD 段,因为一般只有 LOAD 段需要加载到内存中。
所以简单情况下,你也可以通过 readelf 工具查看 so 的对齐状态。
另外,如果你在低版本 NDK (低于 r27) 上使用动态链接到 C++ 标准库,那么可以也会遇到 1ibc++_shared.so
的相关报错,解决的思路还是:
- 升级到 NDK r27
- 将 C++ 标准库静态链接到 so 库里面,例如
-DANDROID_STL=c++_static
externalNativeBuild {cmake {abiFilters 'armeabi-v7a', 'arm64-v8a'arguments '-DANDROID_ARM_NEON=TRUE', '-DANDROID_STL=c++_static'}}
之后,如果 so 运行过程中还存在异常问题, 可以通过地址信息来进行代码定位,例如使用常见的:addr2line
。
而 addr2line
的可执行文件位置,一般位于以下位置:
/Users/guoshuyu/Library/Android/sdk/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin
注意 ndkVersion 的版本还需要 SDK Manager 里有下载安装。
其中 ndk 版本为项目 build.gradle
下配置的 ndk 版本,例如 ndkVersion '21.4.7075529'
,之后你只需要通过执行命令输出,即可看到出现问题的文件和行数:
./aarch64-linux-android-addr2line -e /Users/guoshuyu/workspace/android/··········/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/????.so 000000000007e0a0
当然,也可以增加 -f,通过 ./aarch64-linux-android-addr2line -f -e
来查看出问题的函数是什么:
例如,对于 C++ 里静态存储区域分配,内存在程序编译的时候就已经分配好,而 static 这块内存变量上,如果 so 对齐和地址存在问题,也可能会导致访问地址异常。
如果上述在使用 addr2line
输出时看到都是 ?? ,那么你可能需要修改下编译的调试支持和优化级别,例如 Application.mk
上降低优化等级为 APP_CFLAGS := -O0
。
当然,很多时候存在许多「玄学」的问题,例如前段时间就遇到了一个神奇的 Bug ,甚至也不知道为什么,但是又能改好它。
举个不严谨的例子,通过地址定位,可以定位到报错的地方是这个 static
的全局变量报错,但是为什么会出现 Fatal signal 11(SIGSEGV),code 2(SEGV_ACCERR)
让人费解。
如果按照正常场景来看,C 和 C++ 中的静态变量通常存储在 “data” 区域,但是对于任何未初始化或初始化为零的全局数据,都是存放在 “bss” :
正常情况下,bss 段属于静态内存分配,通常是用来存放未初始化的全局变量和未初始化的局部静态变量,所以 bss 段只是给未初始化的全局变量和未初始化的局部静态变量预留位置而已。
我们可以通过前面的 readelf 工具看一下,如下命令所示 ,通过 -s | grep _errMsg
输出,我们可以看到 _errMsg
确实存在 [23] 的 bss 的 NOBITS 位置。
./aarch64-linux-android-readelf -S /Users/guoshuyu/workspace/*****.so -s | grep _errMsg
但是从输出看,貌似也没什么问题,如果从另外的 .text
和 .rodata
的 Address 上看, 165670 - 071fa0 = F36D0 ,也正好是 4000 的 3C 倍数 ,所以分页应该也没问题。
而通过 objdump 输出的结果看,也和没看出异常,所以问题就变得很玄学,根据猜测,可能是以下某个之一的问题:
./aarch64-linux-android-objdump -h /Users/guoshuyu/workspace/android/****.so
- 某个过程编译的对齐有问题
- NDK 的 gcc 优化存在一些奇怪问题, 例如 gcc 不仅观察变量如何定义,还观察了变量如何在代码中使用
- 赋值的寻址有问题没对齐
- 模拟器系统问题
在偶然之下,屏蔽掉某个使用
_errMsg
的 static function 的static
声明后,它居然就正常运行了,就是偶尔删掉了某个函数的 static 声明,它突然就好了···所以我也不知道是编译的问题还是系统的问题,也许后面有空再深入研究下,不过在适配过程中,真的是「运气好」的情况下,你只需要改一下配置,编译完就可以立即使用,运气不好的情况下,真的就很「玄学」。
最后,如果你需要升级到 NDK r27,你可能还需要对代码的编排方式做一些调整,例如类似 ISO C99 and later do not support implicit function declarations
等小细节问题,因为 NDK r27 目前是 clang-r522817 的版本:
根据 r27 目前的 clang_source_info.md
,其 clang 应该会是 18.1.0 ,也就是包含 C++ 20/23/2C 等的支持,如果升级跨度较大,其实可能会是改了旧坑来新坑的节奏。
例如腾讯目前的 MMKV,在 issue#1353 就提到了兼容的评估问题,虽然对于使用者来说,可能就是几个简单配置就可以重新编译支持,但是对于平台方来说,升级环境是一个“高风险”行为,所以还需要谨慎而行。
最后,这个适配的基础还是你有源码,如果你连源码都没有,那么基本就没希望了,对于 android 环境下,基于 linux 的 4k/16k 内核是不支持混用(详细原因见前文),所以如果你没做适配,很大可能 so 是无法正常运行,不过好消息是,OEM 厂商可以不启用,坏消息是,Google 表示明年在 Google Play 上会强制要求。