前几天刚聊过 《Google 开始正式强制 Android 适配 16 K Page Size》 之后,被问到最多的问题是「怎么查看项目是否支持 16K Page Size」 ?其实有很多直接的方式,但是最难的是当你的项目有很多依赖时,怎么知道这个「不支持的动态库 so」 文件是哪个依赖?有不少人的项目里可能有几十个 so ,如果一个一个那场景可太"有爱"了。
后面的脚本提供查找思路。
首先第一种方法用官方的脚本,保存下方脚本为 shell.sh
,然后执行 ./shell.sh src/main/jinLibs
,就可以检测到目录下所有动态库是否支持 16K:
#!/bin/bash# usage: alignment.sh path to search for *.so filesdir="$1"RED="\e[31m"
GREEN="\e[32m"
ENDCOLOR="\e[0m"matches="$(find $dir -name "*.so" -type f)"
IFS=$'\n'
for match in $matches; dores="$(objdump -p ${match} | grep LOAD | awk '{ print $NF }' | head -1)"if [[ $res =~ "2**14" ]] || [[ $res =~ "2**16" ]]; thenecho -e "${match}: ${GREEN}ALIGNED${ENDCOLOR} ($res)"elseecho -e "${match}: ${RED}UNALIGNED${ENDCOLOR} ($res)"fi
done
整个 Apk 的话,可以直接解压 Apk ,然后对动态库的目录用脚本扫描。
第二种方法就是通过 Google Play 的 app bundle 资源管理器页面直接查看,如果有问题,会看到类似的情况:
另外,如果 so 没问题,但是还是提示你不支持 16 KB,那么很可能是你需要升级 AGP ,建议至少升级到 AGP 8+ ,最优是升级到 8.5.1 之后:
没有问题的情况下是这样:
第三种方法就是下载最新的 libchecker ,如果动态库都有“16 KB” ,那就是正常:
还有一种方法就是使用 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 对齐的状态
注意,是
1000
才是 4K,而10000
是 65536 ,那就是64K 对齐,属于 16K 的 4倍,那「理论上」应该是对齐的
最后,你还可以在 Android Studio 里运行你的 App,然后 Android Studio 会提示存在哪些动态库没适配 16 KB :
注意,目前需要 Android Studio Narwhal ,最新版 Canary 10,并且有个 Bug ,需要首次运行之后,关闭 Android Studio ,然后再次打开,再运行,才会弹出提示:
最后,如果你发现存在动态库不适配,但是你又不知道这个动态库是哪个 aar 远程依赖的,可以通过下方脚本执行 ./gradlew findSoFileOrigins
来输出:
// 在你的模块级别 build.gradle 文件中添加此任务
// 例如: app/build.gradletask findSoFileOrigins {description = "扫描项目依赖的 AAR 文件,找出 .so 文件的来源。"group = "reporting" // 将任务归类到 "reporting" 组下doLast {// 用于存储 AAR 标识符及其包含的 .so 文件路径// 键 (Key): AAR 的字符串标识符 (例如:"project :gsyVideoPlayer", "com.example.library:core:1.0.0")// 值 (Value): 一个 Set 集合,包含该 AAR 内所有 .so 文件的路径 (字符串)def aarSoFilesMap = [:]def variants = nullif (project.plugins.hasPlugin('com.android.application')) {variants = project.android.applicationVariants} else if (project.plugins.hasPlugin('com.android.library')) {variants = project.android.libraryVariants} else {project.logger.warn("警告: findSoFileOrigins 任务需要 Android 应用插件 (com.android.application) 或库插件 (com.android.library)。")return}if (variants == null || variants.isEmpty()) {project.logger.warn("警告: 未找到任何变体 (variants) 来处理。")return}variants.all { variant ->project.logger.lifecycle("正在扫描变体 '${variant.name}' 中的 AAR 依赖以查找 .so 文件...")// 获取该变体的运行时配置 (runtime configuration)def configuration = variant.getRuntimeConfiguration()try {// 配置一个构件视图 (artifact view) 来精确请求 AAR 类型的构件def resolvedArtifactsView = configuration.incoming.artifactView { view ->view.attributes { attributes ->// 明确指定我们只对 artifactType 为 'aar' 的构件感兴趣// AGP 也常用 "android-aar",如果 "aar" 效果不佳,可以尝试替换attributes.attribute(Attribute.of("artifactType", String.class), "aar")}// lenient(false) 是默认行为。如果设为 true,它会尝试跳过无法解析的构件而不是让整个视图失败。// 但如果像之前那样,是组件级别的变体选择失败 (如 gsyVideoPlayer),lenient 可能也无法解决。// view.lenient(false)}.artifacts // 获取 ResolvedArtifactSetproject.logger.info("对于变体 '${variant.name}',从配置 '${configuration.name}' 解析到 ${resolvedArtifactsView.artifacts.size()} 个 AAR 类型的构件。")resolvedArtifactsView.each { resolvedArtifactResult ->// resolvedArtifactResult 是 ResolvedArtifactResult 类型的对象File aarFile = resolvedArtifactResult.file// 获取组件的标识符,这能告诉我们依赖的来源// 例如:"project :gsyVideoPlayer" 或 "com.google.android.material:material:1.7.0"String aarIdentifier = resolvedArtifactResult.id.componentIdentifier.displayNameaarSoFilesMap.putIfAbsent(aarIdentifier, new HashSet<String>())if (aarFile.exists() && aarFile.name.endsWith('.aar')) {// project.logger.info("正在检查 AAR: ${aarIdentifier} (文件: ${aarFile.name})")try {project.zipTree(aarFile).matching {include '**/*.so' // 匹配 AAR 中的所有 .so 文件}.each { File soFileInZip ->aarSoFilesMap[aarIdentifier].add(soFileInZip.path)}} catch (Exception e) {project.logger.error("错误: 无法检查 AAR 文件 '${aarIdentifier}' (路径: ${aarFile.absolutePath})。原因: ${e.message}")}} else {if (!aarFile.name.endsWith('.aar')) {project.logger.debug("跳过非 AAR 文件 '${aarFile.name}' (来自: ${aarIdentifier}),其构件类型被解析为 AAR。")} else {project.logger.warn("警告: 来自 '${aarIdentifier}' 的 AAR 文件不存在: ${aarFile.absolutePath}")}}}} catch (Exception e) {// 这个 catch 块会捕获解析构件视图时发生的错误// 这可能仍然包括之前遇到的 "Could not resolve all artifacts for configuration" 错误,// 如果问题非常根本,即使是特定的构件视图也无法克服。project.logger.error("错误: 无法为配置 '${configuration.name}' 解析 AAR 类型的构件。" +"可能项目设置中存在依赖变体匹配问题," +"详细信息: ${e.message}", e) // 打印异常堆栈以获取更多信息project.logger.error("建议: 请检查项目依赖(尤其是本地子项目如 ':xxxxx')的构建配置," +"确保它们能正确地发布带有标准 Android 库属性(如组件类别、构建类型,以及适用的 Kotlin 平台类型等)的变体。")// 如果希望任务在此处停止而不是尝试其他变体,可以取消下一行的注释// throw e}}// 打印结果if (aarSoFilesMap.isEmpty()) {project.logger.lifecycle("\n在所有已处理变体的可解析 AAR 依赖中均未找到 .so 文件,或者依赖解析失败。")} else {println "\n--- AAR 依赖中的 .so 文件来源 ---"// 按 AAR 标识符排序以获得一致的输出aarSoFilesMap.sort { it.key }.each { aarId, soFileList ->if (!soFileList.isEmpty()) {println "${aarId}:" // 例如:project :gsyVideoPlayer: 或 com.some.library:core:1.0:soFileList.sort().each { soPath -> // 对 .so 文件路径排序println " - ${soPath}" // 例如: - jni/armeabi-v7a/libexample.so}}}println "----------------------------------"}project.logger.lifecycle("任务执行完毕。要再次运行此任务,请执行: ./gradlew ${project.name}:${name}")}
}
这个脚本我是在 APG 8+ 下测试,不同 AGP 可能存在细微 API 差异,思路上一样。
当然,最终还是要在 16 KB 环境运行没有崩溃才行, 在之前的文章我就分享过,很多 so 查看时虽然分页是 16K 或者 64K ,但是它还是有问题的,跑在 16K 上是会崩溃的,具体原因有 NDK 工具可能过老之类:
当时是 NDK10e 等版本,编译出来的 so 都是两个 LOAD 段的 Align 是
10000(65536)
, 也就是 64K 对齐,属于 16K 的 4倍,那「理论上」应该是对齐的,但是跑在 16K 上会 crash ,不过 crash 提示也不是 so 不对齐,而是在某段代码执行时出现 crash,并且你定位到的地址代码会很奇葩。
测试环境可以使用模拟器,一般适配 16 KB 的就是 arm64 ,所以 x86_64 模拟器基本没用,而且需要 Android studio Koala Feature Drop
之后的版本才行:
如果你的 Apk 没适配 16 KB ,那么在 Android 16 的 16 KB 模拟器上会看到这样的提示:
目前在 React Native 和 Flutter 都已经支持了 16 KB:
- Flutter 至少 3.22 版本
- RN 至少 0.77 版本
比如你的 RN 版本太老,就是看到类似下面的场景:
最后,如果你还没适配或者了解 16 KB,可以参考一下文章:
-
Android 15 上适配 16K Page Size 的填坑思路,以 IJKPlayer 为例子
-
Android 15 之如何快速适配 16K Page Size
-
Android 15 适配之16K Page Size :为什么它会是最坑的一个适配点