# T-AND-015:APK native 库体检(ABI / ELF 类型 / 依赖闭包 / 16 KB 页对齐) - **状态**: done - **依赖**: T-AND-012(MediaPipe 初始化崩溃修复)/ T-AND-014(发布包级验收) - **文档**: v1.6 §6.6 / §7.5 - **契约**: 不涉及(打包体检) ## 为什么需要 T-AND-012 的结论是「`ExceptionInInitializerError` 的根因在**集成配置**(native 解压 + R8 + ABI), 不是 MediaPipe 本身不稳定」。但当时的核查是**手工一次性**的:`unzip` 看 `lib/`、`grep` seeds、 `strings` 找类名。手工核查的问题是**下次照样可能漏**,而且漏了也不会有人发现 —— 这类缺陷只会在用户装上手机、点开相机的那一刻暴露。 本任务把它变成**可重复、可反向验证**的产物级检查:纯 Python 标准库解析 ELF, 不依赖 readelf / NDK,因此任何装了 python3 的机器(含 CI)都能跑。 ## 检查项(都在「无设备也能查」的范围内) | 项 | 为什么它会导致「一装上就崩」 | |--|--| | **ABI 与 ELF 类型匹配** | 把 x86_64 的 `.so` 放进 `lib/arm64-v8a/`:安装能过,一加载就 `UnsatisfiedLinkError` | | **`DT_NEEDED` 依赖闭包** | 只打包 `libmediapipe_jni.so` 却没带它依赖的 `libc++_shared.so` 是 MediaPipe 的经典坑;非系统库必须**同 ABI 目录内**自带(系统库由 Android 提供,见脚本内白名单) | | **16 KB 页对齐** | Android 15 起部分设备页大小为 16 KB;`PT_LOAD` 的 `p_align` 若仍是 4 KB(`0x1000`),这些设备上直接加载失败(Google Play 2025-11 起也要求 16 KB 对齐) | | **zip 层对齐** | `zipalign -c -P 16 -v 4`(有 build-tools 时跑) | 实现:`build/check_native_libs.py`(新增,纯标准库)。已接入 `build/accept-release.sh`(发布包)与 `build/verify.sh`(本地构建)。 ## 实测(已发布的 `opendance-android-v0.2.0.zip` 内 APK) ``` [arm64-v8a] ✓ libandroidx.graphics.path.so ELF64 AArch64 | 系统库 3 项 | 页对齐 0x4000 ✓ libimage_processing_util_jni.so ELF64 AArch64 | 系统库 6 项 | 页对齐 0x4000 ✓ libmediapipe_tasks_jni.so ELF64 AArch64 | 系统库 9 项 | 页对齐 0x4000 [armeabi-v7a] ✓ libandroidx.graphics.path.so ELF32 ARM | 系统库 3 项 | 页对齐 0x4000 ✓ libimage_processing_util_jni.so ELF32 ARM | 系统库 6 项 | 页对齐 0x4000 ✓ libmediapipe_tasks_jni.so ELF32 ARM | 系统库 9 项 | 页对齐 0x4000 ✓ native 库体检通过 | ✓ zipalign -c -P 16 -v 4 通过 ``` 结论(对当前产物):**没有缺失的 native 依赖**(`libmediapipe_tasks_jni.so` 只依赖 `libdl/libjnigraphics/libEGL/libGLESv2/libm/libz/liblog/libandroid/libc`,全部系统库), **两个 ABI 都已是 16 KB 页对齐**,ABI 目录内 ELF 类型正确。 也就是说:打包配置这一侧的「一装上就崩」风险已被排除;剩下的只能靠真机实测。 ### 与 readelf 交叉验证 解析器输出与 `LC_ALL=C readelf -d/-lW` 在 6 个 `.so` 上**逐项一致** (`DT_NEEDED` 集合、`PT_LOAD` `p_align`、`e_machine`)。 > **这次交叉验证抓到了解析器自己的一个 bug**:第一版程序头字段偏移写错 —— > ELF64 的 `p_offset/p_vaddr` 之后第三个 QWORD 是 **`p_paddr`** 而不是 `p_filesz` > (ELF32 同理)。后果是 `vaddr → offset` 映射永远失败、`DT_NEEDED` 解析为空, > 于是页面照常打印「✓ 依赖闭包完整(需自带 无;系统库 **0** 项)」—— > 一句**假通过**。`0 项` 这个数字是唯一的破绽,靠和 readelf 比对才暴露出来。 > 教训:**「通过」的输出里必须带可核对的数量/取值**,否则假通过和真通过长得一模一样。 ## 反向验证(三条例都实测会失败) | 负例 | 结果 | |--|--| | 把宿主 x86_64 的 `libz.so.1` 塞进 `lib/arm64-v8a/libbogus.so` | ✗ 三条同时报:`ELF class=2 machine=x86-64,与该 ABI 不符`、`缺少依赖 libc.so.6`、`PT_LOAD p_align=0x1000 … < 16384` | | 把 `libz.so` 从系统库白名单里去掉(证明闭包这条分支是活的) | ✗ `arm64-v8a/libmediapipe_tasks_jni.so:缺少依赖 libz.so`、`armeabi-v7a/...` 同样报 | | `--expected-abis` 传成单个 ABI | ✗ `ABI 集合为 [...],期望 [...]` | ## 仍未覆盖(诚实声明) - **运行时行为**:库能加载 ≠ 推理能跑。GPU/驱动差异、真机内存、连续 30 分钟稳定性仍需设备; - **设备页大小**:这里只保证「16 KB 兼容」,无法证明某台具体设备上的实际行为; - 因此本检查**不替代**真机验收,只是把「打包配置错误」这一类彻底挡在发布前。