如何查看项目是否支持最新 Android 16K Page Size 一文汇总

发布于:2025-05-13 ⋅ 阅读:(15) ⋅ 点赞:(0)

前几天刚聊过 《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 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

整个 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.gradle

task 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,可以参考一下文章


网站公告

今日签到

点亮在社区的每一天
去签到