21、错误处理与调试:常见错误码、日志分析、使用ADB调试NNAPI
说实话,NNAPI 开发最让人头疼的,不是写不出代码,而是跑起来之后一脸懵——模型加载失败、推理结果不对、甚至直接崩溃。我早期做 NNAPI 适配的时候,光是一个 ANEURALNETWORKS_BAD_DATA 就折腾了我整整两天。后来我总结了一套调试方法,今天全部分享给你。
21.1 常见错误码:读懂 NNAPI 的“黑话”
NNAPI 的 API 函数基本都会返回一个状态码。你想想看,如果连返回值都不检查,那跟闭着眼睛开车有什么区别?
下面这张表,是我个人认为最常遇到的几个错误码,建议你收藏一下。
| 错误码 | 值 | 含义 | 常见原因 |
|---|---|---|---|
ANEURALNETWORKS_NO_ERROR |
0 | 一切正常 | —— |
ANEURALNETWORKS_BAD_DATA |
1 | 输入数据格式不对 | 张量维度、类型或缓冲区大小不匹配 |
ANEURALNETWORKS_BAD_STATE |
4 | 状态机异常 | 在编译完成前就执行推理,或者重复执行了某个操作 |
ANEURALNETWORKS_INCOMPLETE |
5 | 操作未完成 | 模型构建过程中缺少必要操作,比如没设置输入张量 |
ANEURALNETWORKS_OP_FAILED |
6 | 算子执行失败 | 硬件不支持该算子,或者算子参数非法 |
ANEURALNETWORKS_OUT_OF_INSUFFICIENT_MEMORY |
7 | 内存不足 | 模型太大,或者设备驱动分配内存失败 |
ANEURALNETWORKS_UNEXPECTED_NULL |
8 | 传入了空指针 | 你忘了初始化某个句柄,或者传了 nullptr |
核心原则:每次调用 NNAPI 函数后,务必检查返回值。不要偷懒,否则你会在调试上花掉 10 倍的时间。
21.2 日志分析:从 logcat 里“挖”线索
我习惯在调试 NNAPI 时,同时开三个 logcat 过滤器。为什么?因为 NNAPI 的日志分散在好几个 TAG 里,只看一个很容易漏掉关键信息。
21.2.1 关键 TAG
- NNAPI:框架层日志,能看到模型编译、执行的生命周期。
- NeuralNetworks:驱动层日志,能看到具体硬件(比如 DSP、NPU)的调用情况。
- System:系统级错误,比如内存分配失败、权限问题。
举个例子,你可能会看到这样的日志:
// 模型编译失败
E NNAPI: [ANEURALNETWORKS_BAD_DATA] Model compilation failed: operand type mismatch at index 3
// 驱动层报错
E NeuralNetworks: [nnapi] nnapi_implementation::execute: OP_FAILED, op_code=29
// 内存不足
E System: mmap failed: Cannot allocate memory
嗯,这里要注意:op_code=29 对应的是哪个算子?你可以去 NNAPI 头文件里查 ANEURALNETWORKS_ADD、ANEURALNETWORKS_CONV_2D 这些枚举值。我一般会写个小脚本,把枚举值和数字对应起来,省得每次翻头文件。
21.2.2 日志分析三步法
- 先看 NNAPI TAG:确认是框架层报错,还是驱动层报错。
- 再看错误码:如果是
BAD_DATA,优先检查张量维度;如果是OP_FAILED,优先检查算子兼容性。 - 最后看上下文:日志里通常会附带操作序号、张量索引,顺着这些索引去检查你的模型构建代码。
小技巧:用 adb logcat -s NNAPI NeuralNetworks 只显示这两个 TAG,避免被其他日志刷屏。
21.3 使用 ADB 调试 NNAPI:实战三板斧
ADB 不只是用来装 APK 的。我个人觉得,ADB 是调试 NNAPI 最趁手的工具,没有之一。
21.3.1 第一板斧:查看设备支持的 NNAPI 能力
你写的模型,目标设备到底能不能跑?别猜,直接查:
adb shell dumpsys nnaapi capabilities
这条命令会输出当前设备支持的所有算子、性能数据、内存限制。我在项目中遇到过,某款手机号称支持 NNAPI,结果跑 ANEURALNETWORKS_QUANTIZED_16BIT_LSTM 直接返回 OP_FAILED。一查 capabilities,果然不支持。
21.3.2 第二板斧:强制指定设备
NNAPI 默认会选一个“最佳”设备。但有时候它选的不对。你可以用 ADB 强制指定:
// 强制使用 CPU
adb shell setprop debug.nnapi.partition 1
// 强制使用 GPU
adb shell setprop debug.nnapi.partition 2
// 强制使用 DSP/NPU
adb shell setprop debug.nnapi.partition 3
为什么要这么做?我曾经遇到一个 bug,模型在 CPU 上跑得好好的,一上 GPU 就崩溃。通过强制指定设备,我很快定位到是 GPU 驱动对某个算子的实现有 bug。
注意:这些属性是调试用的,不要在生产环境里设置。另外,不同厂商的设备编号可能不一样,建议先查一下文档。
21.3.3 第三板斧:抓取详细执行日志
如果上面的方法还不够,那就开启 NNAPI 的详细日志:
adb shell setprop debug.nnapi.trace 1
adb logcat -b all | grep NNAPI
开启后,你会看到每个算子的输入输出张量、执行时间、内存地址。信息量很大,但排查疑难杂症时非常有用。我一般只在问题复现时才开,平时关掉,否则日志刷得太快。
21.4 知识体系总览
下面这张图,是我对 NNAPI 错误处理与调试的总结。你可以把它当成一张“作战地图”。
21.5 避坑指南:我踩过的那些坑
最后,分享几个我亲身经历过的坑,希望能帮你省点时间。
- 坑一:返回值检查不彻底。 我曾经只检查了
ANeuralNetworksCompilation_create的返回值,却没检查ANeuralNetworksExecution_setInput。结果模型编译成功,执行时却因为输入维度不对崩溃了。嗯,从那以后我养成了“每调用必检查”的习惯。 - 坑二:日志里看到
OP_FAILED就以为是驱动 bug。 其实很多时候,是你传的参数不对。比如ANEURALNETWORKS_CONV_2D的 padding 参数,我填错过一次,驱动直接报错。先检查自己的代码,再怀疑驱动。 - 坑三:ADB 强制指定设备后忘了恢复。 调试完记得把
debug.nnapi.partition清掉,否则后续测试都会跑在同一个设备上,测不出问题。
我的习惯:写一个 checkNnapiStatus 工具函数,把错误码转成可读的字符串,再带上文件名和行号。这样日志一打出来,我立刻知道是哪行代码出了问题。
好了,关于 NNAPI 的错误处理与调试,今天就聊到这儿。记住,调试不是浪费时间,而是帮你更深入理解系统。下次遇到 ANEURALNETWORKS_BAD_DATA,别慌,按今天的方法一步步来,肯定能搞定。