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 的新增项随下次发布进包。
74 lines
4.6 KiB
Markdown
74 lines
4.6 KiB
Markdown
# 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 兼容」,无法证明某台具体设备上的实际行为;
|
||
- 因此本检查**不替代**真机验收,只是把「打包配置错误」这一类彻底挡在发布前。
|