Files
opendance-android/tasks/015-native-libs.md
T
zijunchiang 95429e8ce3 test(native): APK native 库体检——ELF 类型 / DT_NEEDED 闭包 / 16 KB 页对齐(T-AND-015)
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 的新增项随下次发布进包。
2026-09-17 05:25:04 +08:00

74 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 兼容」,无法证明某台具体设备上的实际行为;
- 因此本检查**不替代**真机验收,只是把「打包配置错误」这一类彻底挡在发布前。