From 95429e8ce3746d2bb40a3d1b1ed7f2ce746f13e2 Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Thu, 17 Sep 2026 05:25:04 +0800 Subject: [PATCH] =?UTF-8?q?test(native):=20APK=20native=20=E5=BA=93?= =?UTF-8?q?=E4=BD=93=E6=A3=80=E2=80=94=E2=80=94ELF=20=E7=B1=BB=E5=9E=8B=20?= =?UTF-8?q?/=20DT=5FNEEDED=20=E9=97=AD=E5=8C=85=20/=2016=20KB=20=E9=A1=B5?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=EF=BC=88T-AND-015=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 的新增项随下次发布进包。 --- README.md | 14 ++ README.txt | 5 + build/accept-release.sh | 26 ++++ build/check_native_libs.py | 289 +++++++++++++++++++++++++++++++++++++ build/verify.sh | 19 +++ project.yaml | 4 + tasks/015-native-libs.md | 73 ++++++++++ 7 files changed, 430 insertions(+) create mode 100644 build/check_native_libs.py create mode 100644 tasks/015-native-libs.md diff --git a/README.md b/README.md index f9c44e7..c2d15dc 100644 --- a/README.md +++ b/README.md @@ -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`。发布前对包再跑一次: diff --git a/README.txt b/README.txt index b75850f..a2e0afc 100644 --- a/README.txt +++ b/README.txt @@ -138,6 +138,11 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评 $ANDROID_HOME/build-tools/34.0.0/apksigner verify app/build/outputs/apk/release/app-release.apk 预期:退出码 0(无输出即通过) + 0a) native 库体检(T-AND-015;无设备也能查「一装上就崩」的打包问题) + python3 build/check_native_libs.py app/build/outputs/apk/release/app-release.apk + 预期:「✓ native 库体检通过」——每个 ABI 下每个 .so 的 ELF 类型、 + 依赖闭包(DT_NEEDED,非系统库必须同 ABI 自带)、页对齐(16 KB 兼容)。 + 0) 发布包级验收(对**发布包**而不是本地构建;文档 §7.5) bash build/accept-release.sh dist-release/opendance-android-v0.2.0.zip 预期末行「发布包级验收全部通过」。它会校验 sha256、解压、核对文件清单、 diff --git a/build/accept-release.sh b/build/accept-release.sh index 3e4b7f6..81c15f5 100755 --- a/build/accept-release.sh +++ b/build/accept-release.sh @@ -118,6 +118,32 @@ for abi in arm64-v8a armeabi-v7a; do fi done +# ---- 4b. native 库体检(T-AND-015:ABI / ELF 类型 / 依赖闭包 / 16 KB 页对齐)---- +log "native 库体检(ELF 类型 / DT_NEEDED 闭包 / 16 KB 页对齐):" +NPY="${PYTHON_BIN:-}" +if [ -z "$NPY" ]; then + for candidate in python3 python; do command -v "$candidate" >/dev/null 2>&1 && { NPY="$candidate"; break; }; done +fi +if [ -n "$NPY" ]; then + "$NPY" "$PROJECT_DIR/build/check_native_libs.py" "$APK" \ + --expected-abis "$(echo "$ABIS" | tr '\n' ',')" || fail "native 库体检未通过" +else + fail "未找到 python3,无法做 native 库体检(apt-get install -y python3)" +fi + +# 16 KB 页对齐的 zip 层检查(Android 15 起部分设备页 16 KB) +SDK_FOR_ZIPALIGN="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}" +ZIPALIGN="$(ls -d "$SDK_FOR_ZIPALIGN"/build-tools/*/zipalign 2>/dev/null | sort -V | tail -1)" +if [ -n "$ZIPALIGN" ]; then + if "$ZIPALIGN" -c -P 16 -v 4 "$APK" >"$WORK_DIR/zipalign.txt" 2>&1; then + echo " ✓ zipalign -c -P 16 -v 4 通过(4 字节对齐 + 16 KB 页对齐)" + else + fail "zipalign -P 16 校验失败(见 $WORK_DIR/zipalign.txt)" + fi +else + log "未找到 zipalign,跳过 zip 对齐检查(不影响结论)" +fi + # ---- 5. 模型资源与 R8 保留 ---- log "MediaPipe 模型与 R8 保留:" if unzip -l "$APK" | grep -q "assets/pose_landmarker_lite.task"; then diff --git a/build/check_native_libs.py b/build/check_native_libs.py new file mode 100644 index 0000000..e790789 --- /dev/null +++ b/build/check_native_libs.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +"""APK native 库体检(T-AND-015):纯标准库解析 ELF,不需要 readelf / NDK。 + +为什么需要它(T-AND-012 的「先按 ABI / native / R8 排查」): + +`ExceptionInInitializerError` / `UnsatisfiedLinkError` 这类「一装上就崩」的问题, +根因绝大多数落在 native 库上,而它们**都能在没有设备的情况下查出来**: + +1. **ABI 与 ELF 机器类型不匹配** —— 把 x86_64 的 .so 放进 `lib/arm64-v8a/`, + 安装能过、一加载就崩; +2. **DT_NEEDED 依赖缺失** —— 例如只打包了 `libmediapipe_jni.so` 却没带 + `libc++_shared.so`(MediaPipe 早期版本的经典坑)。系统库由 Android 提供, + 非系统库必须**同 ABI 目录内**自带; +3. **16 KB 页面对齐** —— Android 15 起部分设备页大小为 16 KB,`PT_LOAD` 的 + `p_align` 若仍是 4 KB(0x1000),这些设备上会直接加载失败(Google Play + 2025-11 起也要求 16 KB 对齐); +4. **路径/命名**:.so 必须直接位于 `lib//` 下(子目录不会被安装)。 + +本脚本把这些一次查完,并给出可读结论。**它不能替代真机**(运行时行为、 +GPU/驱动差异仍需设备),但能把「打包配置错误」这一类彻底挡在发布前。 + +用法: + python3 build/check_native_libs.py [--expected-abis arm64-v8a,armeabi-v7a] + [--min-page-align 16384] [--json] +退出码:0 通过 / 1 有问题 / 2 用法或文件问题。 +""" + +from __future__ import annotations + +import argparse +import json +import struct +import sys +import zipfile + +# Android 系统提供的库(不需要 App 自带) +SYSTEM_LIBS = { + "libc.so", "libm.so", "libdl.so", "liblog.so", "libz.so", "libstdc++.so", + "libandroid.so", "libjnigraphics.so", "libEGL.so", "libGLESv1_CM.so", + "libGLESv2.so", "libOpenSLES.so", "libOpenMAXAL.so", "libnativewindow.so", + "libvulkan.so", "libsync.so", "libcutils.so", "libutils.so", "libui.so", + "libgui.so", "libbinder.so", "libhardware.so", "libcamera2ndk.so", + "libmediandk.so", "libaaudio.so", "libneuralnetworks.so", "libOpenCL.so", +} + +# ABI → 期望的 (ELF class, e_machine) +ABI_MACHINE = { + "arm64-v8a": (2, 183), # ELFCLASS64, EM_AARCH64 + "armeabi-v7a": (1, 40), # ELFCLASS32, EM_ARM + "x86_64": (2, 62), # ELFCLASS64, EM_X86_64 + "x86": (1, 3), # ELFCLASS32, EM_386 +} + +MACHINE_NAME = {183: "AArch64", 40: "ARM", 62: "x86-64", 3: "Intel 80386"} + + +class ElfError(Exception): + pass + + +# 人类可读输出;--json 模式下静音,保证 stdout 是**纯 JSON**(便于脚本消费) +EMIT = True + + +def emit(*parts) -> None: + if EMIT: + print(*parts) + + +def parse_elf(data: bytes) -> dict: + """解析出 {elf_class, machine, is_64, needed[], soname, page_aligns[]}""" + if len(data) < 64 or data[:4] != b"\x7fELF": + raise ElfError("不是 ELF 文件(魔数 %r)" % data[:4]) + elf_class = data[4] # 1=32bit, 2=64bit + data_encoding = data[5] # 1=LE, 2=BE + if data_encoding != 1: + raise ElfError("只支持小端 ELF(EI_DATA=%d)" % data_encoding) + is_64 = elf_class == 2 + if not is_64 and elf_class != 1: + raise ElfError("未知的 EI_CLASS=%d" % elf_class) + + machine = struct.unpack_from(" len(data): + raise ElfError("程序头越界") + p_type, = struct.unpack_from(" int | None: + for v, o, sz in loads: + if v <= vaddr < v + sz: + return o + (vaddr - v) + return None + + strtab_off = vaddr_to_offset(strtab_vaddr) if strtab_vaddr is not None else None + if strtab_off is not None: + limit = strtab_off + (strsz or 0) + for d_tag, d_val in entries: + if d_tag not in (1, 14): # DT_NEEDED / DT_SONAME + continue + start = strtab_off + d_val + end = data.find(b"\0", start, limit if limit > start else len(data)) + if end < 0: + continue + name = data[start:end].decode("utf-8", "replace") + if d_tag == 1: + needed.append(name) + else: + soname = name + + return { + "elf_class": elf_class, + "machine": machine, + "needed": needed, + "soname": soname, + "page_aligns": page_aligns, + } + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(add_help=True) + parser.add_argument("apk") + parser.add_argument("--expected-abis", default="arm64-v8a,armeabi-v7a") + parser.add_argument("--min-page-align", type=int, default=16384) + parser.add_argument("--json", action="store_true") + args = parser.parse_args(argv[1:]) + + expected_abis = [a.strip() for a in args.expected_abis.split(",") if a.strip()] + + global EMIT + EMIT = not args.json + + try: + zf = zipfile.ZipFile(args.apk) + except (OSError, zipfile.BadZipFile) as exc: + print("✗ 无法读取 APK:%s" % exc, file=sys.stderr) + return 2 + + libs: dict[str, list[str]] = {} + for name in zf.namelist(): + parts = name.split("/") + if len(parts) == 3 and parts[0] == "lib" and name.endswith(".so"): + libs.setdefault(parts[1], []).append(parts[2]) + for abi in libs: + libs[abi].sort() + + problems: list[str] = [] + notes: list[str] = [] + + if not libs: + print("✗ APK 里没有任何 lib//*.so", file=sys.stderr) + return 1 + + found_abis = sorted(libs.keys()) + if found_abis != sorted(expected_abis): + problems.append("ABI 集合为 %s,期望 %s" % (found_abis, sorted(expected_abis))) + + report: dict = {"abis": {}} + emit(" APK native 库体检:%s" % args.apk) + for abi in found_abis: + emit(" [%s]" % abi) + entries = [] + abi_files = set(libs[abi]) + for lib in libs[abi]: + raw = zf.read("lib/%s/%s" % (abi, lib)) + try: + info = parse_elf(raw) + except ElfError as exc: + problems.append("%s/%s:%s" % (abi, lib, exc)) + emit(" ✗ %-32s %s" % (lib, exc)) + continue + + want_class, want_machine = ABI_MACHINE.get(abi, (None, None)) + machine_name = MACHINE_NAME.get(info["machine"], str(info["machine"])) + if want_machine is not None and (info["elf_class"], info["machine"]) != (want_class, want_machine): + problems.append( + "%s/%s:ELF class=%d machine=%s,与该 ABI 不符(该 ABI 下会加载失败)" + % (abi, lib, info["elf_class"], machine_name) + ) + emit(" ✗ %-32s ELF 类型不符:class=%d machine=%s" % (lib, info["elf_class"], machine_name)) + else: + emit(" ✓ %-32s ELF%s %s" % (lib, "64" if info["elf_class"] == 2 else "32", machine_name)) + + # 依赖闭包:非系统库必须在同 ABI 目录内自带 + missing = [ + dep for dep in info["needed"] + if dep not in SYSTEM_LIBS and dep not in abi_files + ] + if missing: + problems.append("%s/%s:缺少依赖 %s(同 ABI 目录内没有)" % (abi, lib, ", ".join(missing))) + emit(" ✗ 缺少依赖:%s" % ", ".join(missing)) + else: + extra = [d for d in info["needed"] if d not in SYSTEM_LIBS] + emit(" ✓ 依赖闭包完整(需自带 %s;系统库 %d 项)" % ( + ", ".join(extra) if extra else "无", len(info["needed"]) - len(extra))) + + # 16 KB 页面对齐 + bad_align = [a for a in info["page_aligns"] if a < args.min_page_align] + if bad_align: + problems.append( + "%s/%s:PT_LOAD p_align=%s < %d,Android 15+ 的 16 KB 页设备会加载失败" + % (abi, lib, ", ".join(hex(a) for a in bad_align), args.min_page_align) + ) + emit(" ✗ 页对齐不足:%s" % ", ".join(hex(a) for a in bad_align)) + else: + aligns = sorted(set(info["page_aligns"])) + emit(" ✓ 页对齐 %s(≥ %d,16 KB 页兼容)" % ( + " ".join(hex(a) for a in aligns), args.min_page_align)) + + entries.append({"lib": lib, **{k: info[k] for k in ("machine", "elf_class", "needed", "soname")}, + "page_aligns": info["page_aligns"]}) + report["abis"][abi] = entries + + # libc++ 共享库的经典坑:若某个 .so 需要它,必须自带 + for abi, libs_here in libs.items(): + if "libc++_shared.so" in libs_here: + notes.append("%s 自带 libc++_shared.so" % abi) + + report["problems"] = problems + report["notes"] = notes + if args.json: + print(json.dumps(report, ensure_ascii=False, indent=2)) + + if problems: + print("") + for p in problems: + print(" ✗ %s" % p, file=sys.stderr) + print(" ✗ native 库体检发现 %d 个问题" % len(problems), file=sys.stderr) + return 1 + + emit(" ✓ native 库体检通过:%s | ELF 类型匹配、依赖闭包完整、16 KB 页对齐" % ", ".join(found_abis)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/build/verify.sh b/build/verify.sh index 88698ce..1e31af5 100755 --- a/build/verify.sh +++ b/build/verify.sh @@ -118,6 +118,25 @@ else fi done + # ---- T-AND-015:native 库体检(ABI / ELF 类型 / 依赖闭包 / 16 KB 页对齐)---- + NPY="${PYTHON_BIN:-python3}" + if command -v "$NPY" >/dev/null 2>&1; then + # 期望 ABI 用上面已经算好的 EXPECTED_ABIS(项目声明的真机 ABI), + # 而不是 APK 自己的 ABI 列表 —— 否则「ABI 是否符合预期」这条就自我循环了 + if "$NPY" "$PROJECT_DIR/build/check_native_libs.py" "$RELEASE_APK" \ + --expected-abis "$(echo "$EXPECTED_ABIS" | tr '\n' ',' | sed 's/,$//')" \ + >/tmp/apk_native.txt 2>&1; then + grep -E "✓ native 库体检通过" /tmp/apk_native.txt | sed 's/^/ /' + else + echo "✗ native 库体检未通过:" + sed 's/^/ /' /tmp/apk_native.txt | tail -20 + FAILED=1 + fi + else + echo "✗ 未找到 $NPY,无法做 native 库体检" + FAILED=1 + fi + # ---- T-AND-012:R8 不得裁掉 MediaPipe(seeds 由 -printseeds 输出) ---- SEEDS="app/build/outputs/mapping/release/seeds.txt" if [ -f "$SEEDS" ]; then diff --git a/project.yaml b/project.yaml index 1ddbb96..0ce992f 100644 --- a/project.yaml +++ b/project.yaml @@ -82,6 +82,10 @@ tasks: file: tasks/014-release-acceptance.md scope: 发布包级端到端验收(读 zip 内 APK:版本元数据 / ABI+MediaPipe native / 模型资源 / R8 / 契约快照) status: done + - id: T-AND-015 + file: tasks/015-native-libs.md + scope: APK native 库体检(ELF 类型 / DT_NEEDED 闭包 / 16 KB 页对齐),纯标准库实现 + status: done deliverable: artifact: opendance-android-v0.2.0.apk diff --git a/tasks/015-native-libs.md b/tasks/015-native-libs.md new file mode 100644 index 0000000..c141097 --- /dev/null +++ b/tasks/015-native-libs.md @@ -0,0 +1,73 @@ +# 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 兼容」,无法证明某台具体设备上的实际行为; +- 因此本检查**不替代**真机验收,只是把「打包配置错误」这一类彻底挡在发布前。