运行时与集成

Android 本地 AI JNI 异常边界与 Kotlin 错误契约

不要把原生错误文本直接抛到 UI;按阶段映射错误并验证异常后的资源清理。

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

直接回答

JNI 边界应将失败转成可预测的应用契约,而不是把原生细节直接暴露给 UI,也不能在资源失效时继续运行。Android NDK 文档说明,JNI 调用可能留下待处理的托管异常;异常待处理时大多数 JNI 函数不能继续调用;C++ 异常也不能跨 JNI 转换边界进入托管代码。这不只是代码风格:如果不检查待处理异常,后续 JNI 操作可能无效;若 C++ 异常越界,可能产生未定义行为或进程终止。 为每个桥接操作定义错误契约。明确哪些失败以类型化状态/结果返回,哪些托管异常可以传播,缓冲区和原生句柄由谁拥有,每种退出路径如何清理。模型加载、后端设置、推理、取消、输出转换和关闭错误应可区分。不能把所有失败压成空字符串或“AI 出错”。可用于支持的原生错误类别和阶段应脱敏;不要在异常文本里带原始提示词、转写、文件路径或认证数据。 在调用点遵循 JNI 语义。原生代码调用托管方法后,继续执行其他 JNI 操作前要检查是否有待处理异常;方法抛错时,其返回值无效。Pinned 数组、字符串、局部/全局引用和锁要在所有路径释放。不要跨线程复用 JNIEnv 指针;线程附加/分离和归属需符合官方 JNI 规则。若 Kotlin/Java API 收到原生错误,应映射成应用拥有的类型状态,并在重试启动前确认引擎/会话清理完成。 用注入故障测试边界,不要等待生产崩溃。覆盖原生初始化失败、回调抛托管异常、分配失败、句柄失效、调用期间取消、输出转换失败以及部分初始化后关闭。每种情形验证清理恰好执行一次、没有陈旧句柄、返回错误分类明确,并且允许的下一项任务可以从干净状态启动。可在受控调试/测试构建中使用 CheckJNI 等文档工具,发布版则另行确认诊断不会泄露私密内容。 本文是设计与测试方法,不是某个运行时 bug 的事实声明。Cove ModelManager 目前有显式生命周期状态,但这些状态不能证明所有 JNI 适配路径都具备异常安全性。要测试实际集成并说明可观察范围。本地推理也不证明诊断日志私密或应用完全不联网;错误遥测要尽量少,并单独审计网络行为。 错误契约可以避免两个极端:把底层堆栈直接展示给用户,或把所有失败吞掉并伪装成成功的空结果。每种类型错误应对应用户可采取的下一步,例如稍后重试、修复模型、检查权限或联系支持。底层句柄失效后,桥接层要阻止后续任务继续使用旧状态。若无法保证清理成功,就将引擎置为不可用并要求重新初始化,而不要在未知状态下继续推理。 代码审查时应检查异常待处理状态是否在 native callback、方法调用或资源释放边界得到处理,并验证任何异常清除都有明确目的。清除异常不等于恢复操作成功;代码应记录原始失败类别,再安全返回。测试要覆盖局部引用与全局引用生命周期、JNIEnv 线程绑定和 native 资源释放。用户日志只需包含错误码、阶段和请求 ID,不应携带模型输入输出。

首次原生调用报错后,下一次请求仍然崩溃

Kotlin 页面调用本地推理包装层。第一次请求返回错误,但第二次请求崩溃或表现得像复用了陈旧引擎状态。原生代码可能留有待处理托管异常,或提前返回时未做清理。

决策

定义类型化 JNI-Kotlin 错误边界,检查待处理异常,并为每种退出路径测试资源清理与句柄状态。

预期结果

每种失败都变成稳定应用状态,原生资源仅清理一次,后续允许的请求不会复用无效状态。

需要收集的证据

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

  • COVE-LIFECYCLE-01:当前 ModelManager 有显式模型生命周期状态,但这不能证明所有 JNI 适配路径都安全。
  • PRIVACY-01:本地推理不需要向远程推理服务器发送输入,但不代表应用无其他网络请求或日志天然安全。

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

实施流程

  1. 绘制每个 JNI 操作与所有者. 为每次桥接调用列出托管/原生输入、线程、对象/句柄所有者、缓冲生命周期、异常来源及清理负责人。引擎和会话所有权不能绑在不稳定的 UI 回调上。
  2. 定义类型化错误和异常策略. 决定哪些原生失败返回状态/结果,哪些托管异常允许传播。原生代码调用托管方法后先检查待处理异常;绝不能让 C++ 异常穿过 JNI 边界。
  3. 审查所有出口的资源释放. 检查局部/全局引用、固定数组/字符串、锁、原生句柄和部分初始化状态。验证提前返回、异常、取消和关闭路径只释放其拥有的资源,且只做一次。
  4. 注入边界故障. 测试回调异常、初始化失败、无效句柄、内存分配失败、取消、输出转换和部分关闭。断言错误阶段、类型结果、清理次数,以及下一次允许操作从干净状态开始。
  5. 保持诊断私密且可行动. 向应用返回错误类别和阶段,同时脱敏提示词、媒体、凭据和私有路径。在受控测试中按文档使用 CheckJNI 等调试检查,并另行验证发布行为。

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

验收标准

  • C++ 异常不会跨 JNI 进入托管层;待处理 Java 异常后不会继续调用普通 JNI 操作。
  • 每个桥接操作对句柄、引用、缓冲和锁有明确所有者及清理契约。
  • 注入故障返回类型化且带阶段的结果,不留下供后续任务误用的引擎/会话句柄。
  • 用户可见和诊断错误不含私密输入/输出,除非经单独审阅的授权流程明确需要。

决策对照表

维度选择或证据解读方式
将原始原生异常文本展示给 UI避免内容不稳定且可能敏感,也没有定义资源清理和重试语义。
类型化桥接结果将已知失败类别和阶段映射到应用状态提供稳定 UI、恢复动作和测试断言,同时保留有效分类。
无条件清除任何待处理异常不应丢弃而不处理可能隐藏原始原因;应按文档检查并处理,或遵循契约返回。

需要避免的失败模式

  • C++ 异常穿过 JNI 边界,或待处理 Java 异常后继续调用任意 JNI 函数。
  • 捕获错误后立即重试,但没有检查句柄、引用和缓冲是否清理。

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

最小复现记录

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

发布检查表

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

来源与相关证据

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

常见问题

能否通过 JNI 把 C++ 异常抛给 Kotlin?

Android NDK 指南说明 C++ 异常不能跨 JNI 转换边界。应按契约将失败转成状态值或采用明确的托管异常策略,并保留错误阶段、恢复状态和清理结果。

Java 异常待处理时,原生代码能否继续调用 JNI?

大多数 JNI 调用在异常待处理时不允许继续。应检查异常状态,按桥接契约处理或返回,然后再进行后续调用;不可忽略失败结果。

捕获异常是否保证引擎可以安全重试?

不能。仍需单独验证清理结果和句柄状态。应测试部分初始化、资源释放及下一次调用后,再把重试描述为安全,并设定明确恢复条件。