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

前几天刚聊过 《Google 开始正式强制 Android 适配 16 K Page Size》 之后,被问到最多的问题是「怎么查看项目是否支持 16K Page Size」 ?其实有很多直接的方式,但是最难的是当你的项目有很多依赖时,怎么知道这个「不支持的动态库 so」 文件是哪个依赖?有不少人的项目里可能有几十个 so ,如果一个一个那场景可太"有爱"了。

后面的脚本提供查找思路。

首先第一种方法用官方的脚本,保存下方脚本为 shell.sh ,然后执行 ./shell.sh src/main/jinLibs ,就可以检测到目录下所有动态库是否支持 16K:

sh 复制代码
#!/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,通过以下命令可以输出对应参数:

bash 复制代码
./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 来输出:

groovy 复制代码
// 在你的模块级别 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,可以参考一下文章

相关推荐
独立开阀者_FwtCoder3 分钟前
"页面白屏了?别慌!前端工程师必备的排查技巧和面试攻略"
java·前端·javascript
Touper.7 分钟前
JavaSE -- 泛型详细介绍
java·开发语言·算法
sun0077009 分钟前
std::forward作用
开发语言·c++·算法
Hilaku11 分钟前
说实话,React的开发体验,已经被Vue甩开几条街了
前端·javascript·vue.js
星语卿11 分钟前
Js事件循环
javascript
vocal11 分钟前
【我的安卓第一课】Android 多线程与异步通信机制(1)
android
datagear11 分钟前
如何在DataGear 5.4.1 中快速制作HTTP数据源服务端分页的数据表格看板
javascript·数据可视化
顾林海13 分钟前
ViewModel 销毁时机详解
android·面试·android jetpack
namehu19 分钟前
“什么?视频又双叒叕不能播了!”—— 移动端视频兼容性填坑指南
javascript·html
多啦C梦a21 分钟前
React Hooks 编程:`useState` 和 `useEffect`,再不懂就OUT了!
前端·javascript