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 的新增项随下次发布进包。
This commit is contained in:
2026-09-17 05:25:04 +08:00
parent c207e7dadd
commit 95429e8ce3
7 changed files with 430 additions and 0 deletions
+14
View File
@@ -21,6 +21,7 @@ OpenDance Studio **阶段三**: Android 辅助 App(Kotlin + CameraX + MediaPipe
| 模型随包分发 | OK `assets/pose_landmarker_lite.task` 5.7 MB(Stored 未压缩) |
| ABI 覆盖 | OK arm64-v8a + armeabi-v7a(debug 额外含 x86/x86_64) |
| APK 版本元数据 | OK 直接读产物 `AndroidManifest` 的 `versionName=0.2.0`/`versionCode=200`,与 `project.yaml` 一致(T-AND-013) |
| native 库体检 | OK ELF 类型匹配双 ABI、`DT_NEEDED` 闭包完整(无缺失依赖)、`PT_LOAD` 页对齐 `0x4000`(16 KB 页兼容)、`zipalign -P 16` 通过(T-AND-015) |
## 快速开始
@@ -256,6 +257,19 @@ app/src/main/java/com/opendance/studio/
(= `project.yaml` 的 `version` = APK 文件名 = Git tag),契约版本在构建期从
`contracts/VERSION` 解析后注入 `BuildConfig`;Kotlin 界面代码里不出现任何版本号字面量。
**native 库体检(T-AND-015)**:`ExceptionInInitializerError` 这类「一装上就崩」的
问题大多出在 native 库打包上,而这几项**无设备也能查**:
```bash
python3 build/check_native_libs.py dist-release/opendance-android-v0.2.0/app-release.apk
# -> 每个 ABI 下每个 .so 的 ELF 类型 / DT_NEEDED 闭包 / PT_LOAD 页对齐
# -> ✓ native 库体检通过:arm64-v8a, armeabi-v7a | ELF 类型匹配、依赖闭包完整、16 KB 页对齐
```
纯 Python 标准库实现(不需要 readelf / NDK),已接入 `verify.sh` 与 `accept-release.sh`。
与 `readelf` 在 6 个 `.so` 上逐项交叉验证一致;三条检测分支都用真实负例反向验证过
(错 ABI 的 .so、缺失依赖、页对齐不足)。**它不替代真机**,只把「打包配置错误」挡在发布前。
**发布包级验收(T-AND-014)**:`build/verify.sh` 验的是本地刚构建的 APK,
而用户装的是 zip 里的 `app-release.apk`。发布前对包再跑一次: