【Bug已解决】Android: Linux-targeted libonnxruntime.so (from runtimes/linux-arm64) gets bundled into APK instead of the Android AAR's library 解决方案
一、现象长什么样
在 Android 项目里集成 ONNX Runtime(通过官方 AAR,内含libonnxruntime.so的arm64-v8a/armeabi-v7a版本),构建 APK 后安装运行报UnsatisfiedLinkError或加载了错误的.so,排查发现 APK 里打包的竟是Linux 主机用的libonnxruntime.so(来自runtimes/linux-arm64,是 x86_64 Linux 的库),而不是 Android AAR 里的 Android 版.so。现象:
# 现象 A:运行时加载失败 # java.lang.UnsatisfiedLinkError: dlopen failed: ... # "/vendor/lib64/libonnxruntime.so" has unexpected e_machine / wrong ELF class # (其实是 Linux x86_64 的 ELF,Android arm64 加载器拒绝) # 现象 B:APK 里混入了 linux-arm64 的 .so # unzip -l app.apk | grep onnxruntime # 看到 libonnxruntime.so 来自 runtimes/linux-arm64(x86_64),不是 AAR 的 # 现象 C:只在特定 gradle 配置/依赖顺序触发 # 当项目同时依赖了某个拉入 runtimes/linux-arm64 的包,且它的 .so # 被 gradle 的 jniLibs 合并规则优先选中,就覆盖掉 AAR 的 Android .so最坑的是现象 C:正常只依赖 AAR 时没问题,一旦项目里某个其他依赖(或本地runtimes/目录)把 linux-arm64 的.so也带进 jniLibs 合并,gradle 的“后者覆盖/按字母序”规则可能让 Linux 版胜出,APK 里就是错的库,且只有真机运行才暴露。
二、背景
Android 的 native 库(.so)通过 AAR 的jni/目录提供,构建时 gradle 把各 AAR 的jni/<abi>/libxxx.so合并进 APK。ONNX Runtime 的 Android AAR 内含jni/arm64-v8a/libonnxruntime.so等 Android 版。
问题在于:开发机上的 ORT 仓库里还有runtimes/linux-arm64/libonnxruntime.so(这是给开发机 Linux x86_64用的,名字叫linux-arm64但其实是 x86_64 Linux 二进制,或者开发者误把主机库当 arm 库)。如果构建脚本/某个依赖把runtimes/目录也加进了jniLibs.srcDirs,gradle 合并时就会把这个 Linux.so带进 APK,且因合并顺序/规则覆盖了 AAR 的 Android 版,导致 APK 打包了错误的库。
这是 Android 打包/原生库合并审查里典型的坑:jniLibs 源目录包含了非 Android 的.so,合并时被优先选中,覆盖了正确的 AAR 库。
三、根因
runtimes/linux-arm64被加进 jniLibs 源:构建脚本误把开发机用的 Linux.so目录设为jniLibs.srcDirs,导致它被合并进 APK(现象 B)。合并覆盖规则让 Linux 版胜出:gradle 多源合并时,后添加/字母序靠后的源覆盖先前的,Linux
.so盖掉了 AAR 的 Android.so(现象 C)。缺少 APK 内容校验:CI 没检查 APK 里
.so的架构(ELF e_machine),错误库长期存在。
本质:是jniLibs 源误含非 Android 的 Linux.so且合并覆盖规则使其胜出,且缺 APK 架构校验。
四、最小可运行复现
下面用 Python 模拟“jniLibs 多源合并,Linux .so 覆盖 AAR 的 Android .so”:
def merge_jnilibs_buggy(aar_libs, extra_src_dirs): """buggy: 后者覆盖前者,Linux .so 盖掉 AAR 的 Android .so。""" result = {} # AAR 先放 for abi, so in aar_libs.items(): result[abi] = so # extra 源(含 linux-arm64 的 x86_64 .so)后放,覆盖 for d in extra_src_dirs: for abi, so in d.items(): result[abi] = so # 覆盖! return result aar = {"arm64-v8a": "android_libonnxruntime.so"} linux_dir = {"arm64-v8a": "linux_x86_64_libonnxruntime.so"} # 名字同 abi 但其实是 Linux x86_64 out = merge_jnilibs_buggy(aar, [linux_dir]) print("buggy bundled:", out["arm64-v8a"]) # linux_x86_64 版(错!) def merge_jnilibs_fixed(aar_libs, extra_src_dirs): """fixed: 拒绝非 Android 架构的 .so 进入合并。""" result = dict(aar_libs) for d in extra_src_dirs: for abi, so in d.items(): if "linux" in so or "x86_64" in abi: continue # 跳过非 Android 库 result[abi] = so return result out2 = merge_jnilibs_fixed(aar, [linux_dir]) print("fixed bundled:", out2["arm64-v8a"]) # android 版(对)buggy被 Linux 版覆盖,fixed跳过非 Android 库,保留 AAR 版。
五、解决方案(第一层:最小直接修复)
最小修复:构建脚本里不要把runtimes/下的 Linux.so目录加进jniLibs.srcDirs,且对进入合并的.so做架构白名单过滤(只允许 Android abi):
// 修正:只把 AAR 的 jni 作为 native 库源,排除开发机 runtimes 目录 android { sourceSets { main { // 不要写 jniLibs.srcDirs += 'runtimes/linux-arm64' jniLibs.srcDirs = ['src/main/jniLibs'] // 仅 AAR 提供 } } }并在合并时过滤:任何含linux/x86_64的.so不进入 APK。
这一层改动最小:移除 Linux 目录 + 架构白名单,APK 恢复正确的 Android.so。但依赖“每处 jniLibs 配置都审查”,下看第二层。
六、解决方案(第二层:结构性改进)
把“APK native 库必须来自 Android AAR、排除非 Android .so”固化成单一事实来源。下面这个 dataclass 集中管理合并契约:
from dataclasses import dataclass, field from typing import Dict, List @dataclass class OrtAndroidSoPolicy: """单一事实来源:Android APK native 库合并契约。""" ALLOWED_ABIS = ("arm64-v8a", "armeabi-v7a", "x86", "x86_64") def merge(self, aar_libs: Dict[str, str], extra_src_dirs: List[Dict[str, str]]) -> Dict[str, str]: result = {k: v for k, v in aar_libs.items() if k in self.ALLOWED_ABIS} for d in extra_src_dirs: for abi, so in d.items(): # 只允许 Android abi 且非 Linux 主机库进入 if abi in self.ALLOWED_ABIS and "linux" not in so: result[abi] = so return result def assert_android_only(self, bundled: Dict[str, str]) -> None: for abi, so in bundled.items(): if abi not in self.ALLOWED_ABIS or "linux" in so: raise AssertionError(f"non-Android .so bundled: {abi}/{so}")这一层的关键收益:
- abi 白名单:只接受 Android abi,Linux 主机库被排除;
- AAR 优先:以 AAR 库为基础,extra 源仅补充合法项;
- 断言:
assert_android_only校验 APK 内无 Linux.so; - 单一事实来源:所有 APK native 库合并约定收口在
OrtAndroidSoPolicy。
七、解决方案(第三层:断言 / CI 守护)
把第二层钉成 pytest,挂进 CI,作为 APK 内容门禁:
import pytest from your_package.ort_android_so import OrtAndroidSoPolicy def test_linux_so_excluded(): # 断言 1:linux-arm64 的 .so 被排除,保留 AAR 的 Android 版 p = OrtAndroidSoPolicy() out = p.merge({"arm64-v8a": "android_libonnxruntime.so"}, [{"arm64-v8a": "linux_x86_64_libonnxruntime.so"}]) assert out["arm64-v8a"] == "android_libonnxruntime.so" def test_only_android_abis(): # 断言 2:最终只含 Android abi p = OrtAndroidSoPolicy() out = p.merge({"arm64-v8a": "a.so", "x86": "b.so"}, []) assert set(out.keys()) <= set(p.ALLOWED_ABIS) def test_non_android_caught(): # 断言 3:含 linux 的 .so 必被断言抓出 p = OrtAndroidSoPolicy() with pytest.raises(AssertionError): p.assert_android_only({"arm64-v8a": "linux_libonnxruntime.so"}) def test_aar_preferred(): # 断言 4:AAR 库始终保留(不被 extra 覆盖除非 extra 合法) p = OrtAndroidSoPolicy() out = p.merge({"arm64-v8a": "aar.so"}, [{"armeabi-v7a": "extra_legal.so"}]) assert out["arm64-v8a"] == "aar.so" assert out["armeabi-v7a"] == "extra_legal.so"四条断言从“Linux 排除”“仅 Android abi”“非 Android 被抓”“AAR 优先”四面把打包错误钉死在 CI。
八、排查清单
Android 集成 ORT 运行报UnsatisfiedLinkError/加载错库时:
unzip -l app.apk | grep onnxruntime看.so来自哪——是否混入了runtimes/linux-arm64的 Linux 库(现象 B)。- 构建脚本的
jniLibs.srcDirs是否误含开发机runtimes/目录?有就移除(现象 C)。 - 合并顺序是否让 Linux
.so覆盖了 AAR 的 Android.so?加架构白名单过滤。 - 用第二层
OrtAndroidSoPolicy:abi 白名单 + AAR 优先 + 非 Android 断言。 - 加第三层 pytest,断言“Linux 排除、仅 Android abi、非 Android 被抓、AAR 优先”。
- Android 打包的 native 库必须只来自 AAR 的 Android 版,任何
runtimes/linux-*主机库都不能进 jniLibs。
九、小结
Android 集成 ORT 打包错库,本质是构建脚本把开发机用的runtimes/linux-arm64(x86_64 Linux).so加进了jniLibs.srcDirs,gradle 合并时被其覆盖 AAR 的 Android 版.so,导致 APK 里是 Linux 库、真机加载失败;且缺 APK 架构校验。修复分三层——第一层移除 Linux 目录 + 加 abi 白名单;第二层用OrtAndroidSoPolicy这个 dataclass 把“仅 Android abi + AAR 优先 + 非 Android 断言”收口成单一事实来源;第三层用四条 pytest 把“Linux 排除、仅 Android abi、非 Android 被抓、AAR 优先”钉死在 CI。核心心法:Android 打包的 native 库必须只来自 AAR 的 Android 版,任何runtimes/linux-*主机库都不能进 jniLibs,否则会被合并覆盖成错误架构。