T-AND-012 的结论是「ExceptionInInitializerError 的根因在集成配置(native 解压 +
R8 + ABI),不是 MediaPipe 本身不稳定」,但当时的核查是**手工一次性**的(unzip 看
lib/、grep seeds、strings 找类名)——下次照样可能漏,且漏了也没人发现,只会在用户
装上手机、点开相机那一刻暴露。
新增 build/check_native_libs.py(纯 Python 标准库解析 ELF,不需要 readelf / NDK),
检查三项**无设备也能查**的「一装上就崩」根因:
* ABI 与 ELF 类型匹配(把 x86_64 的 .so 放进 lib/arm64-v8a/ → 一加载就崩);
* DT_NEEDED 依赖闭包(非系统库必须同 ABI 目录内自带;MediaPipe 早期版本的
libc++_shared.so 就是这么缺的);
* PT_LOAD 页对齐 ≥ 16 KB(Android 15 起部分设备页 16 KB,4 KB 对齐会直接加载失败;
Google Play 2025-11 起也要求)。
外加 zipalign -c -P 16 -v 4(有 build-tools 时)。已接入 verify.sh 与 accept-release.sh。
实测(已发布的 opendance-android-v0.2.0.zip 内 APK):
双 ABI 下 6 个 .so 全部 ELF 类型匹配、依赖闭包完整(libmediapipe_tasks_jni.so 只依赖
libdl/libjnigraphics/libEGL/libGLESv2/libm/libz/liblog/libandroid/libc,全是系统库)、
PT_LOAD p_align=0x4000(16 KB 兼容)、zipalign -P 16 通过。
结论:打包配置这一侧的「一装上就崩」风险已排除,剩下的只能靠真机实测。
与 readelf 交叉验证(6 个 .so 的 DT_NEEDED / p_align / e_machine 逐项一致)——
这次交叉验证抓到解析器自己的 bug:第一版程序头偏移写错(ELF64 的
p_offset/p_vaddr 之后第三个 QWORD 是 p_paddr 而非 p_filesz),导致 vaddr→offset
映射失败、DT_NEEDED 解析为空,页面照样打印「✓ 依赖闭包完整(系统库 0 项)」——
一句**假通过**,唯一破绽就是那个 0。教训:通过输出必须带可核对的数量/取值。
反向验证(三条分支都用真实负例证明会失败):
* 注入宿主 x86_64 的 libz.so.1 到 lib/arm64-v8a/libbogus.so → 同时报 ELF 类型不符、
缺依赖 libc.so.6、页对齐 0x1000 < 16384;
* 把 libz.so 从系统库白名单去掉 → 报缺少依赖 libz.so(证明闭包分支是活的);
* --expected-abis 传错 → 报 ABI 集合不符。
verify.sh 全绿;对**已发布资产**重新下载后跑完整 accept-release.sh 全部通过。
未新发版本:APK 本身未变(本任务是检查工具与文档),README 的新增项随下次发布进包。
4.6 KiB
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 兼容」,无法证明某台具体设备上的实际行为;
- 因此本检查不替代真机验收,只是把「打包配置错误」这一类彻底挡在发布前。