运行时与集成

Android 本地 AI 原生库加载顺序与故障定位

聚焦原生库依赖与初始化的先后和首个错误边界,不与 ABI 拆包或 R8 keep 规则混淆。

审核状态: 内容已校验;中文稿供用户审核 · 2026-10-05 · Fact IDs: COVE-LIFECYCLE-01, MEMORY-03

直接回答

原生库有依赖关系和初始化行为,应让它们在应用的 runtime 设计中可观察。Android JNI 官方指南介绍使用 System.loadLibrary 加载 native 代码,并建议放在静态初始化器或其他有意安排的早期所有者中。关键不是“多加载几个库直到能跑”,而是确定哪个库提供某个方法、它依赖什么,以及最早失败在哪一步。模型 runtime 可能依赖多个共享对象,应用层还可能有 JNI 回调或厂商 delegate。文件缺失、动态依赖无法解析、重复初始化或 native 方法注册失败会表现为不同错误,不能合并为一个模糊的“模型失败”。 改代码前先画依赖图。记录不带平台前缀的库名、由哪个模块或依赖打包,以及首次调用路径。还要区分应用包内库与系统提供或仅特定后端需要的可选库。不能假设每台手机上都有某个库,也不能为了压制警告就把可选组件声明为必需。官方 System.loadLibrary API 说明传入的名字格式;NDK JNI 说明加载和注册行为。但这些资料不能证明某个 Cove 制品具体包含哪些 native 对象。 选择一个安全的初始化所有者,让它执行一次有意的初始化并保留结果状态。不要把 System.loadLibrary 放在 Composable 副作用、反复创建的屏幕构造函数或多个并发入口中。由应用或 runtime adapter 管理初始化,记录成功或失败,并阻止后续任务静默重复执行尚未清理完的初始化。如果允许重试,先定义要重置哪些状态和资源。所有者不能吞掉 UnsatisfiedLinkError;应保留首个失败类型及去内容化阶段标识,从而分辨“找不到库”“找到但无法打开”“JNI 注册失败”以及“方法存在但内部抛错”。 测试必须针对实际安装的制品,而不只是开发机上的构建目录。比较 debug/release 包含的库,检查 APK 或设备定向 split 中实际存在的对象,并在同一进程执行冷加载和重复请求。可控测试构建中可加入缺依赖夹具,确认应用输出边界清楚的错误,不会把未完成的 engine 标成 ready。如果只有混淆后找不到 callback,应另查 R8;如果设备 ABI 不包含库,应查交付;如果方法能找到但抛错,就查 JNI 异常边界,不要仅改变加载顺序。 有用的诊断应保留构建变体、应用版本、runtime 修订、库名、ABI、首次加载阶段、异常类别及 runtime owner 是否清理,但不能记录提示词、图像、音频或完整私有文件路径。某个测试能证明特定构建的加载顺序,却不能证明普遍设备兼容或推理性能。尤其是一次 System.loadLibrary 成功只说明一个库在该进程中完成映射,并不证明模型制品、delegate 或任务都准备好了。 将恢复设计成状态转换。加载失败时按所有权释放部分创建对象,呈现不可用状态,并阻止请求进入未达到 ready 的 native 对象。重试要由用户或明确策略触发、有次数边界且可观察,不能在每次组合或应用返回前台时循环执行。后续尝试成功后,只清除该尝试自己的错误状态,并让新 session 绑定新初始化的 runtime。这能避免旧失败尝试的迟到回调改变当前状态。 多个库同时存在时,不要添加官方文档未说明的顺序假设。优先调用所属 runtime 的公开初始化流程;只有官方明确要求时才自行预加载内部依赖。把应用层顺序写成测试:进程启动、owner 初始化、engine 创建、任务执行、关闭以及可选的重新初始化。它是可验证序列,不代表所有库都使用相同的全局初始化机制。当前事实卡仅指出生命周期与内存边界,并未给出 Cove 的 linker trace,因此结论只能基于实际观察到的包内容和可重现实验。 错误分类也应影响 UI。用户无需看到动态链接器完整诊断,但应知道功能暂不可用及可执行的下一步。只有在明确且安全时才提供重试;如果必须更新应用或重新安装,应说明该选项,不应让用户反复点按钮。日志仅保存可用于工程排查的类别化证据,并对路径和安装标识做最小化处理。 验收时分别记录:native 对象是否在制品内、平台是否映射成功、JNI 接口是否注册、引擎是否初始化以及任务是否完成。某一步失败并不意味着后续状态成立。将同一制品摘要、ABI 和系统版本附在结果上,后续才可以比较。没有完整原始记录时,不推广成“所有 Android 设备兼容”或“初始化修复完成”。清楚保留边界比依赖错误消息的表面文本更可靠。

冷启动时模型引擎尚未就绪就出现 native 加载失败

release 构建启动本地 AI 功能。适配器加载一个 native 组件后,在任务提交前收到 loader 错误;另一个屏幕也可能尝试初始化。

决策

基于已安装制品追踪依赖与 owner 顺序,分类首个失败点,阻止第二个初始化器掩盖原始错误。

预期结果

用户看到明确的不可用状态,团队能区分交付、链接、注册和任务失败。

需要收集的证据

只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:

  • COVE-LIFECYCLE-01:项目事实支持显式模型 runtime 生命周期测试,不提供 Cove 设备 linker 实测轨迹。
  • MEMORY-03:native partial state 与恢复边界需要观察;不能把失败的半初始化 engine 标成就绪。

只有记录了模型身份、构建配置、实际后端、任务样例和失败情况,运行时比较才可复核。

实施流程

  1. 盘点真实 native 依赖图. 记录每个库名、打包所有者、必要依赖、平台可选属性和首次调用者。区分 APK 自带对象与系统提供或条件使用的 native 库。
  2. 选择唯一初始化所有者. 把加载放到 runtime adapter 或应用级 owner。UI 创建和重组只观察初始化状态,不独立触发库加载。
  3. 保留最早的 loader 错误. 在恢复或重试前捕获异常类别和阶段。区分文件缺失、打开/链接失败、JNI 注册或符号查找以及方法内部抛错。
  4. 针对已安装制品复现. 检查 APK 与设备定向 split 中的库和 ABI,再从安装后的 debug/release 制品执行冷加载与同进程重复加载。
  5. 断言失败清理和受控重试. 部分初始化失败时保持 engine 不可用,关闭自有临时资源;重试次数受控,并确认旧回调不能把新的初始化尝试误报为 ready。

让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。

验收标准

  • 实际构建与 runtime 修订对应的依赖图和初始化 owner 已记录。
  • 保留首个失败类别,不把包交付、链接、JNI 注册和任务异常压成一个错误。
  • 部分初始化失败后不暴露 ready engine,也不接受推理请求。
  • 重试与关闭有边界、有所有者且针对安装制品测试;诊断日志不包含用户内容。

决策对照表

维度选择或证据解读方式
安装包中没有 native 库检查 APK/split 内容与 ABI 交付属于交付边界;改变 runtime 加载顺序不能补上缺少的对象。
库存在但平台无法打开检查依赖链接错误与加载阶段属于 native 映射或依赖问题,应保留首个异常细节。
库已加载但方法或 callback 失败查 JNI 注册、R8 或异常边界映射成功;需分别判断符号可见性和方法行为。

需要避免的失败模式

  • 在每个界面进入时重复调用 native 加载,或忽略 UnsatisfiedLinkError。
  • 把 APK 未包含库、动态链接失败、JNI 方法找不到和方法内部异常归为同一模型错误。

还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。

最小复现记录

层级记录内容
设备厂商、型号、芯片、内存档位、操作系统版本
软件应用版本和 Git 提交号
模型系列、变体、版本、格式、文件长度、摘要、量化方式
运行时名称、版本、请求后端及实际后端证据
工作负载样例版本、输入摘要、提示词摘要、输出策略
结果完成、失败、取消、回退及保护隐私的诊断信息

发布检查表

  • ☐ 主要搜索问题已回答,且没有未经支持的数字。
  • ☐ 所有模型文件和运行时版本均已固定。
  • ☐ 代表性任务和失败处理策略均有说明。
  • ☐ 生命周期、取消、清理和回退均已测试。
  • ☐ 用户内容与网络边界均有说明。
  • ☐ 结论只适用于被记录的配置。
  • ☐ 新增测试结果时链接到原始方法或证据。

来源与相关证据

中文部署与排错专题见。奇连 AI 端侧专题。

常见问题

是否应在应用启动时手动加载所有 native 依赖?

不应一概而论。遵循所属 runtime 的公开初始化路径;手动预加载内部库会引入未文档化顺序假设,并可能隐藏首个错误及原始异常。

一次 loadLibrary 成功就说明可以推理了吗?

不说明。它只证明一个对象在该进程完成映射;模型验证、engine 初始化、backend 设置和任务接受仍是不同门禁,必须逐步确认并记录证据来源。

每次进入屏幕都自动重试可以吗?

不建议。重试需有次数边界、部分资源清理、唯一 owner 和可观察的新尝试;Composable 重组不应触发它,旧回调也要忽略并核对状态更新及归属。