如何查看项目是否支持最新 Android 16K Page Size 一文汇总
前几天刚聊过 《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; 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)\" fidone
整个 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 = null if (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 // 获取 ResolvedArtifactSet project.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.displayName aarSoFilesMap.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 :为什么它会是最坑的一个适配点