# 嵌入式 MCU Service 代码审查框架 v2.0

> 适用于 **MCU 通信服务 + JNI 桥接 + Android Service + AIDL IPC** 类项目的系统性代码审查。
> 覆盖 Java / C(C++) / 协议 / 驱动全栈，以及 **语言层陷阱** 和 **JNI 边界陷阱**。
> 项目完成后，按此框架逐条排查，可拦截 85% 以上的隐藏缺陷。

---

## 框架总览

```
┌─────────────────────────────────────────────────────────────┐
│  维度 1: 数据流图    ── 数据从哪来、到哪去、变了几手         │
│  维度 2: 时序图      ── 多线程怎么交互、锁在哪里、竞态窗口   │
│  维度 3: 状态机      ── 状态怎么转、漏了哪些、超时兜底       │
│  维度 4: 决策树      ── 函数分支全覆盖、错误码一致性         │
├─────────────────────────────────────────────────────────────┤
│  维度 5: 资源所有权  ── 谁创建、谁释放、泄漏与悬空           │
│  维度 6: 故障传播链  ── 一个底层错误怎么击垮整个系统         │
│  维度 7: 跨层契约    ── Java/C 常量、超时、缓冲区是否对得上  │
│  维度 8: 时延预算    ── 端到端时延拆解、瓶颈识别             │
├─────────────────────────────────────────────────────────────┤
│  附录 C: C 语言陷阱  ── 整数溢出、符号混用、UB、宏副作用     │
│  附录 D: Java 陷阱   ── 匿名类泄漏、异常吞没、时间溢出       │
│  附录 E: JNI 陷阱    ── 引用泄漏、异常未清、Modified UTF-8   │
└─────────────────────────────────────────────────────────────┘
```

**使用方式**：
1. 新功能开发完成后，由作者按 **8 个维度 + 3 个附录** 自审一遍。
2. Code Review 时，Reviewer 从 8 个维度中抽取与改动最相关的 2~3 个维度重点审查；如果改动涉及 JNI 或 C 层，必须同时过 **附录 C/E**。
3. 发布前，用 **附录 B（Checklist 汇总表）** 做全量回归。

---

## 维度 1：数据流图 — 从 MCU 到 App

### 解决什么问题
数据在跨层传递过程中是否**丢失、篡改、拷贝遗漏、格式错误**。

### 什么时候画
- 新增一条数据通路时（如新增传感器上报）
- 修改 payload 结构体 / AIDL 接口时
- JNI 层修改了数组传递方式时

### 画什么
```
[MCU] → [UART/ SPI/ CAN] → [C Reader] → [JNI] → [Java Router]
   → [Feature] → [Broadcast] → [Binder IPC] → [App]
```
每一段标注：
1. **数据格式**（原始字节 / byte[] / int / Parcelable / struct）
2. **是否有拷贝**（memcpy / clone / NewByteArray / Parcel.write）
3. **线程名**（哪个线程在执行这段代码）
4. **是否有队列**（Handler.post / EventQueue / BlockingQueue）
5. **生命周期**（buffer 何时失效、谁负责释放）

### 检查清单

- [ ] **拷贝完整性**：JNI 传来的 `jbyteArray` 在回调结束后是否可能被回收？C 层是否还在用指针？
- [ ] **零拷贝机会**：是否有不必要的拷贝可以消除？（如直接传递 `ByteBuffer` 地址）
- [ ] **线程切换点**：耗时操作（Binder IPC、文件 I/O）是否在 reader 线程上直接执行，阻塞了串口读取？
- [ ] **数据格式转换**：C 的 `uint8_t` 到 Java `int` 是否做了 `& 0xFF`？有无符号扩展问题？
- [ ] **大小端**：多字节字段（如 `uint16_t freq`）在 C 层和 Java 层的解析是否一致？
- [ ] **边界值**：payload 长度为 0、为最大长度、为 null 时，各层行为是否一致？
- [ ] **生命周期**：C 层的 `malloc` buffer 是否在 Java 层异常时泄漏？`NewByteArray` 是否有对应的 `DeleteLocalRef`？

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| JNI `GetByteArrayElements` 后没有 `Release...` | Native 内存泄漏或 GC 阻塞 | grep 两个成对出现 |
| C 层 buffer 在回调返回后立即释放，但 Java 层异步使用 | 随机崩溃 / 数据损坏 | 检查 buffer 持有者与释放者是否同线程 |
| `byte[]` 在 Java 层被修改后，C 层期望只读 | 数据竞争 | 确认 `ReleaseByteArrayElements` 的 mode 参数 |
| 大 payload 在 Binder IPC 中传输 | `TransactionTooLargeException` | AIDL 接口中检查 `body.length` 上限 |

### 产出模板
```markdown
### 数据流：XXX 上报
```
[MCU] --(uart raw)--> [C reader] --(McuFrame)--> [JNI] --(jbyteArray)--> [FrameRouter]
--(byte[] copy)--> [VehicleFeature] --(VehicleSnapshot)--> [Binder] --(Parcel)--> [App]
```

| 环节 | 格式 | 线程 | 拷贝 | 生命周期 |
|------|------|------|------|---------|
| C reader | uint8_t[256] | live_reader | 无（栈buffer） | 函数返回即失效 |
| JNI notify | jbyteArray | C reader (attached) | NewByteArray | DeleteLocalRef 后释放 |
| FrameRouter | byte[] copy | FrameRouter HT | clone() | 由 Handler Message 引用 |
| Feature | byte[] | FrameRouter HT | 无（引用） | 同线程内使用 |
| Binder | Parcel | Binder 线程池 | Parcel.write | IPC 后回收 |

- **风险点**：XXX
- **建议**：XXX
```

---

## 维度 2：时序图 — 多线程并发

### 解决什么问题
多线程共享变量时是否存在**竞态条件、死锁、锁粒度过大/过小**。

### 什么时候画
- 新增线程（HandlerThread、pthread、线程池）时
- 新增 `synchronized` / `pthread_mutex_t` / `volatile` 时
- 修改单例模式时
- 出现 ANR / 卡死 / 随机崩溃时

### 画什么
```
时间轴(↓)   线程A             线程B             线程C
t1         写 snapshot.x=1
           lock(mutex)
           ...
           unlock(mutex)
t2                           读 snapshot()
t3                                           写 snapshot.x=2
```
画清楚：
1. **竖线** = 线程/执行上下文
2. **横条** = 代码行（精确到变量赋值或函数调用）
3. **锁的范围** = 哪个时间段锁被持有（用 `[====]` 表示）
4. **队列操作** = push / pop / wait / notify

### 检查清单

- [ ] **锁的范围**：锁是否只保护了最小临界区？是否存在"锁了写 + 等 ACK"的大锁？
- [ ] **锁的对象**：`synchronized(this)` 和 `synchronized(snapshot)` 是两把不同的锁，调用方是否混淆？
- [ ] **读是否需要锁**：Java 引用赋值是原子的，但复合操作（读多个字段）不是。是否用了 `volatile` 或 `AtomicReference`？
- [ ] **双重检查锁定（DCL）**：如果用了单例 DCL，`instance` 是否声明为 `volatile`？
- [ ] **C 层锁顺序**：是否存在 A→B 和 B→A 的锁获取顺序，导致死锁？
- [ ] **锁的释放路径**：异常抛出时（Java `try-finally` / C `pthread_cleanup_push`），锁是否一定释放？
- [ ] **无锁设计的正确性**：`CopyOnWriteArrayList`、`ConcurrentHashMap` 是否用对了场景？
- [ ] **线程泄漏**：`HandlerThread`、`pthread` 在 Service `onDestroy` 时是否 `quitSafely` / `pthread_join`？

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| `synchronized` 块内调 Binder IPC | ANR | 审查所有 sync 块内的调用 |
| C 层 `mutex_lock` 后 `return` 没有 `unlock` | 死锁 | 用脚本检查每个 lock 后所有 exit path |
| Java `volatile boolean` 做复合操作（如 `flag = !flag`） | 竞态 | 替换为 `AtomicBoolean` 或加锁 |
| `RemoteCallbackList.beginBroadcast` 后 crash，没调 `finishBroadcast` | 后续广播异常 | 用 `try...finally` 包裹 |
| JNI 全局锁 `g_link_mutex` + C 层 `ctx->lock` 嵌套，但反向也成立 | 死锁 | 检查所有锁的嵌套顺序是否一致 |

### 产出模板
```markdown
### 时序：snapshot 读写竞态

```
时间轴(↓)   FrameRouter线程      App Binder线程

t1         synchronized(snapshot) {
               snapshot.voltageMv = 12000;
           }
t2                              snapshot() {
                                   synchronized(snapshot) {
                                       return new VehicleSnapshot(snapshot);
                                   }
                                }
```

- **锁对象**：`snapshot` 实例本身
- **范围**：仅字段赋值 / 构造函数拷贝
- **竞态窗口**：无（读写同一把锁）
- **风险**：无
- **优化建议**：读操作频繁时，可考虑 ReadWriteLock
```

---

## 维度 3：状态机 — 有状态模块

### 解决什么问题
模块在状态转换时是否**漏状态、非法转换未处理、超时未兜底**。

### 什么时候画
- C 层有 `enum State` / `typedef enum` 时
- Java 层有 `STATE_XXX` 常量或 `switch(state)` 时
- 新增 OTA / 连接 / 充电等流程时

### 画什么
```
       事件A             事件B
[状态1] ─────→ [状态2] ─────→ [状态3]
  ↑                          │
  └──────── 事件C ────────────┘
```
每个转换标注：
1. **触发条件**（收到什么帧、超时、用户操作、IO 结果）
2. **守卫条件**（什么前提下才能转，如 `if (running)`）
3. **副作用**（write、callback、日志、释放资源）
4. **超时**（该状态下等待外部事件的最大时间）

### 检查清单

- [ ] **状态完整性**：是否穷举了所有可能的状态？用 `default` 分支兜底了吗？
- [ ] **非法转换**：在 `STATE_IDLE` 收到 `DATA` 帧会怎样？是静默忽略还是报错？
- [ ] **超时兜底**：每个"等待外部事件"的状态都有超时处理吗？超时后是重试、回退还是报错？
- [ ] **重入安全**：状态转换函数可能被中断/重入吗？（如 OTA 中收到新的 OTA 请求）
- [ ] **副作用一致性**：转换失败时，已经执行的副作用（如 UART 写、内存分配）是否回滚？
- [ ] **状态可见性**：Java 层的 `isXxx()` 方法是否准确反映了 C 层真实状态？是否存在"假活"？

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| 状态机无超时，永久阻塞 | 功能卡死 | 检查每个 wait/event 是否有 deadline |
| `default: break` 吞掉非法事件 | 调试困难，行为诡异 | 改为 `default: log+error` |
| 状态转换中抛异常，未回滚已执行操作 | 资源泄漏 | 用 `try...catch...finally` 或事务模式 |
| C 层状态变了，Java 层缓存未更新 | UI 显示错误 | 检查 Java `isXxx()` 是读缓存还是实时 JNI 调用 |

### 产出模板
```markdown
### 状态机：MCU 链路

```
                    send_setup()
        ┌────────────────────────────────────────┐
        │                                        │
        ▼                                        │
   [LINK_IDLE] ──────→ [LINK_SETUP_SENT] ───────┘
        ▲                    │ 收到 SETUPACK
        │                    ▼
        │               [LINK_LINKED]
        │                    │
        │    收到 CLOSE      │ 收到 HB/DATA/BOOT
        └────────────────────┘    → send_ack
        收到 FT_CLOSE
        → link_state=LINK_IDLE
```

| 转换 | 触发 | 守卫 | 副作用 | 超时 |
|------|------|------|--------|------|
| IDLE → SETUP_SENT | open() | !opened | reset seq | 无 |
| SETUP_SENT → LINKED | SETUPACK | seq valid | link_up=true | 3000ms × 3次 |
| LINKED → IDLE | CLOSE | - | link_down=true | 无 |
| LINKED → ? | SETUP | 当前忽略 | - | - |

- **风险**：LINKED 状态下收到重复 SETUPACK 会重置 send_seq
- **建议**：增加守卫 `if (state->link_state == LINK_LINKED) ignore;`
```

---

## 维度 4：决策树 — 函数分支逻辑

### 解决什么问题
多分支函数是否存在**漏分支、错误码混用、异常路径资源未释放**。

### 什么时候画
- 公开 API 方法（AIDL 接口、C 公开函数）
- 有 `switch`、`if-else` 嵌套超过 2 层的函数
- 返回值有多个错误码的函数

### 画什么
```
                  入口
                    │
              ┌─────┴─────┐
          条件A=True   条件A=False
              │              │
         ┌────┴────┐    ┌────┴────┐
      条件B=True   False   ...    ...
```
每个叶子节点标注：
1. **返回值**（错误码、布尔值、seq、null）
2. **副作用**（写日志、发广播、修改全局状态、释放资源）
3. **是否测试覆盖**（单元测试是否命中该分支）

### 检查清单

- [ ] **所有分支都有返回值**：Java 编译器会检查，但 C 不会！手动检查每个退出路径。
- [ ] **错误码一致性**：同一个错误语义是否用了不同的错误码？（如 `-1` 有时表示 timeout，有时表示 link down）
- [ ] **异常路径资源释放**：`try...catch` 或 C 的 `goto error` 路径中，`malloc` 的 buffer、`open` 的 fd、`lock` 是否都释放了？
- [ ] **空指针/空数组**：每个 `byte[]` / `char*` / `jbyteArray` 参数是否为 null 时有处理？
- [ ] **边界值**：数组长度为 0、为 1、为 MAX 时，分支行为是否正确？
- [ ] **默认分支**：`switch` 的 `default` 是报错还是静默忽略？是否合理？

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| C 函数 `switch` 无 `default` | 非法输入导致未定义行为 | 强制每个 switch 有 default |
| 错误码 `-1` 被两处不同语义复用 | 调用方无法区分错误根因 | 建立错误码枚举，一码一事 |
| 异常路径忘记 `free(buffer)` | Native 内存泄漏 | 用 valgrind / asan 跑测试 |
| Java `catch (Exception e) { return -1; }` 吞掉异常 | 调试困难 | 至少打 error 日志 |
| `if (ptr) free(ptr);` 后没有 `ptr = NULL` | 重复释放（double free） | 检查 free 后是否置空 |

### 产出模板
```markdown
### 决策树：McuBinder.setBandWithFreq

```
setBandWithFreq(band, freq)
   │
   ├── try { Commands.Radio.setBand(...) }
   │    └── catch IllegalArgumentException
   │         └── return ERR_INVALID_ARG      [叶子①]
   │
   ├── if (!isLinkAlive())
   │    └── return ERR_LINK_DOWN             [叶子②]
   │
   ├── seq = sendAndWaitAck(...)
   │   ├── seq >= 0
   │   │   └── return OK                     [叶子③]
   │   └── seq < 0
   │       └── return ERR_TIMEOUT            [叶子④]
```

| 叶子 | 返回值 | 副作用 | 测试覆盖 |
|------|--------|--------|---------|
| ① | ERR_INVALID_ARG | 无 | ✅ |
| ② | ERR_LINK_DOWN | 无 | ✅ |
| ③ | OK | UART 写 | ✅ |
| ④ | ERR_TIMEOUT | UART 写 | ✅ |

- **问题**：叶子④ 无法区分 "写失败(seq=-2)" 和 "超时(seq=-1)"
- **建议**：参考 sendCommand 写法，细化错误码
```

---

## 维度 5：资源所有权图 — 谁创建、谁释放

### 解决什么问题
资源（内存、fd、线程、引用）是否存在**泄漏、重复释放、悬空使用**。

### 什么时候画
- 新增 JNI 函数时
- 新增线程、文件、网络连接时
- 修改单例生命周期时
- 出现 OOM / fd 耗尽 / 句柄泄漏时

### 画什么
对每个资源，画出完整的生命周期：
```
[创建] → [使用] → [转移?] → [释放]
   │        │          │         │
   │        │          │         └── 释放点：谁调了 free/close/delete
   │        │          └── 转移点：所有权是否移交给另一个对象？
   │        └── 使用点：哪些函数/线程在使用？是否需要锁保护？
   └── 创建点：malloc/open/NewGlobalRef/pthread_create
```

### 检查清单

- [ ] **内存**：C 的 `malloc/calloc` 是否有对应的 `free`？Java 的 `new` 对象是否会被 GC 根长期引用导致泄漏？
- [ ] **文件描述符**：`open/socket` 是否有 `close`？异常路径是否也 close？
- [ ] **JNI 引用**：`NewGlobalRef` 是否有 `DeleteGlobalRef`？`NewLocalRef` 是否在函数返回前释放？
- [ ] **JNI 数组**：`GetByteArrayElements` 是否 `ReleaseByteArrayElements`？mode 参数是否正确（`0` 还是 `JNI_ABORT`）？
- [ ] **线程**：`pthread_create` / `new HandlerThread` 是否有 `pthread_join` / `quitSafely`？
- [ ] **锁**：`pthread_mutex_init` 是否有 `pthread_mutex_destroy`？`synchronized` 块内抛异常时锁是否释放？（Java 会自动释放）
- [ ] **回调注册**：`registerListener` 是否有对应的 `unregister`？Service 销毁时是否全部反注册？
- [ ] **单例引用**：单例是否长期持有 Activity/Context 引用？（如 `UpgradeListener` 是匿名内部类）

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| `NewGlobalRef` 无 `DeleteGlobalRef` | so 反复加载时泄漏 | grep 配对检查 |
| `GetStringUTFChars` 后 JNI 异常跳转，未 Release | 泄漏 | 用 `JNIEnv` RAII 封装 |
| C 结构体内含指针，free 结构体时未 free 成员 | 泄漏 | 封装 `xxx_free` 函数，统一清理 |
| Java `register` 后未 `unregister`，匿名类持有 Activity | OOM | Service onDestroy 时遍历反注册 |
| `close(fd)` 后未置 `fd = -1`，后续逻辑误用 | crash / 操作错误 fd | free/close 后立即置无效值 |

### 产出模板
```markdown
### 资源：UART fd (ctx->fd)

| 阶段 | 位置 | 操作 | 错误路径处理 |
|------|------|------|-------------|
| 创建 | `serial_open_port` | `fd = open(...)` | 失败时 `close(fd)` |
| 使用 | `serial_write_all` | `write(fd, ...)` | 返回 -1，调用方处理 |
| 使用 | `serial_read_some` | `read(fd, ...)` | 返回 <=0，reader 空转 |
| 释放 | `mcu_link_close` | `close(fd); fd=-1` | idempotent |

- **风险**：`QsMcuSystemService.onDestroy` 中 `waitForRxQuiet` 可能阻塞 3s
- **建议**：评估是否可取消 drain 直接 close
```

---

## 维度 6：故障传播链 / 故障树 — 一个错误怎么击垮系统

### 解决什么问题
底层故障（UART 断、MCU 重启、内存不足）如何**层层放大**，每一层是否**正确处理或及时止损**。

### 什么时候画
- 设计容错策略时
- 出现偶发崩溃/卡死，需要根因分析时
- 新增硬件异常处理逻辑时

### 画什么
```
根因：[UART 物理断开]
   │
   ├── C 层：read() 返回 -1
   │   ├── reader_thread 空转（running 仍为 true）
   │   ├── heartbeat_thread 写失败（不修改状态）
   │   └── isLinked() 仍返回 true（状态机未感知）
   │
   ├── Java 层：isLinkAlive() → true
   │   ├── sendAndWaitAck → write 失败 → rc=-2
   │   └── McuBinder 返回 ERR_TIMEOUT（非 ERR_LINK_DOWN）
   │
   └── App 层：收到 ERR_TIMEOUT → 重试 → 永远失败
```

标注每一层：
1. **错误码/返回值**变了什么
2. **状态**变了什么
3. **是否止损**（如重试、降级、告警）
4. **是否放大**（如阻塞、雪崩、资源耗尽）

### 检查清单

- [ ] **根因检测**：系统能否在最早层发现故障？（如 UART 断开时，C 层是否能立即标记 link down？）
- [ ] **错误码保真**：底层错误码（如 `errno=ENODEV`）传到上层后，是否被篡改或统一抹平？
- [ ] **重试策略**：超时后是否盲目重试？是否有指数退避？最大重试次数？
- [ ] **降级策略**：链路断开后，Service 是否进入降级模式？（如缓存最后状态、拒绝新命令）
- [ ] **恢复策略**：故障恢复后，系统能否自动恢复？（如 MCU 重启后，Service 是否自动重新握手？）
- [ ] **级联阻断**：一个模块故障是否会拖垮其他模块？（如 OTA 卡住时，普通命令是否也卡死？）
- [ ] **日志完整性**：每层故障是否都留下了足够的日志？（日志级别是否正确？）

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| 底层错误被层层抹平，最终变成统一的 "timeout" | 无法诊断根因 | 检查每层是否保留原始错误信息（至少打 log） |
| 链路断开后 isLinkAlive() 仍返回 true | 假活，App 无限重试 | 模拟拔 UART，观察状态变化 |
| 重试无上限，导致请求风暴 | CPU/内存耗尽 | 检查重试次数和退避策略 |
| 一个 waiter 阻塞，导致其他 waiter 饿死 | 功能部分失效 | 检查锁粒度和队列隔离 |
| onDestroy 时被 Binder 调用阻塞 | ANR | 检查销毁路径是否依赖其他线程释放 |

### 产出模板
```markdown
### 故障树：UART 断开

```
[UART 断开]
   ├── C: read() → -1
   │   ├── 结果：reader 空转，不触发 notify
   │   ├── 状态：link_state 仍为 LINKED
   │   └── 止损：❌ 无！isLinked() 仍 true
   │
   ├── Java: isLinkAlive() → true
   │   ├── 结果：App 命令进入 sendAndWaitAck
   │   ├── 错误码：write 失败 → rc=-2 → ERR_TIMEOUT
   │   └── 止损：❌ App 误以为是 MCU 不响应，可能重试
   │
   └── App: 重试 3 次 → 均失败 → 提示用户
       └── 止损：✅ 最终放弃，但浪费了 4.5s
```

- **根因检测失效点**：C 层没有 UART 在线检测机制
- **错误码失真点**：write 失败(-2) 被映射为 ERR_TIMEOUT
- **建议**：增加 HB 写失败检测，连续 N 次失败自动标记 link_down
```

---

## 维度 7：跨层契约一致性表 — Java 和 C 的约定是否对得上

### 解决什么问题
Java 层和 C 层的**常量、超时、缓冲区尺寸、枚举值**是否一致，是否存在"各说各话"。

### 什么时候写
- 新增/修改协议帧格式时
- 新增 AIDL 接口时
- 修改超时值或缓冲区大小时
- 出现 "Java 发得出去，C 层说太大" 或反之的 bug 时

### 写什么
建立三张对照表：

**表 1：协议常量**
| 语义 | Java 值 | C 值 | 是否一致 |
|------|--------|------|---------|
| FRAME_TYPE_DATA | 0x06 | FT_DATA = 0x06 | ✅ |
| MSG_TYPE_PUBLIC | 0x01 | MT_PUBLIC = 0x01 | ✅ |
| ... | ... | ... | ... |

**表 2：缓冲区/长度限制**
| 语义 | Java 限制 | C 限制 | 是否一致 |
|------|----------|--------|---------|
| 最大 payload | body.length > 2040 | MCU_MAX_PAYLOAD_LEN = 2041 | ❌ |
| 最大帧长 | 无 | 2048 | - |

**表 3：超时配置**
| 语义 | Java 值 | C 默认值 | 运行时谁生效 |
|------|--------|---------|-------------|
| ACK 超时 | 1500ms | 2500ms | Java 传入覆盖 |
| SETUP 超时 | 无 | 3000ms | C 层固定 |

### 检查清单

- [ ] **帧类型/命令码**：Java 的 `switch(cmd)` 和 C 的 `enum` 是否覆盖完全一致？
- [ ] **payload 长度**：Java 层的拒绝阈值是否 <= C 层的处理能力？（最好完全相等）
- [ ] **超时值**：Java 传入的超时是否 >= C 层内部子超时之和？（如 Java 给 1000ms，C 内部 wait 1500ms，会提前报 Java 超时）
- [ ] **枚举映射**：AIDL 的 `int` 错误码和 C 的 `enum` 返回值是否一一对应？有无错位？
- [ ] **字符串编码**：Java `String` → C `char*` 时，编码是 UTF-8 还是 ASCII？C 层是否按字节长度截断？
- [ ] **结构体对齐**：C 层 `struct` 如果直接 `memcpy` 到 Java `byte[]`，是否考虑了 padding？
- [ ] **版本兼容性**：AIDL 版本升级后，旧 Client 调用新 Service 的默认值是否安全？

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| Java 限制 2040，C 支持 2041 | 2041 字节帧在 Java 层被拦截 | 对比检查所有 length/size 常量 |
| Java 超时 1000ms < C 内部 2500ms | Java 先超时，C 还在等 | 确保 Java 超时 >= C 内部最大耗时 |
| C `enum` 从 0 开始，Java 从 1 开始 | 状态错位 | 打印双方枚举值对比 |
| C `sizeof(struct)` 含 padding，Java 按紧凑格式解析 | 字段错位 | 禁止直接 memcpy struct，用显式 pack |
| AIDL `int` 错误码新增了一个值，C 没同步 | 调用方收到未知错误码 | 建立错误码对照表，版本升级时同步 review |

### 产出模板
```markdown
### 跨层契约一致性检查

#### 协议常量
| 语义 | Java | C | 一致 |
|------|------|---|------|
| FT_DATA | 0x06 (FrameRouter) | 0x06 (mcu_protocol.h) | ✅ |
| FT_NOTIFY | 0x07 (FrameRouter) | 0x07 FT_TRANSFER (C) | ⚠️ 命名不一致 |

#### 长度限制
| 语义 | Java | C | 一致 |
|------|------|---|------|
| max payload | >2040 拒绝 | 2041 支持 | ❌ 差 1 字节 |

#### 超时
| 语义 | Java | C | 运行时 |
|------|------|---|--------|
| ACK | 1500ms | 2500ms | Java 覆盖 C |

- **风险**：max payload 差 1 字节
- **建议**：统一为 2040 或 2041
```

---

## 维度 8：性能时延预算 — 端到端时延拆解

### 解决什么问题
关键路径的**时延是否满足产品需求**，瓶颈在哪里，优化是否有效。

### 什么时候写
- 新增数据通路时（如新增高频传感器）
- 修改 UART 波特率、缓冲区大小、线程模型时
- 出现卡顿、ANR、OTA 太慢等性能问题时
- 产品定义了明确的响应时间 KPI（如 "按键后 100ms 内响应"）

### 写什么
对每条关键路径，建立时延预算表：

```
[触发] → [环节A] → [环节B] → [环节C] → [结果可见]
   │        │          │          │
   │      Tt/Tp      Ts/Tq     Tproto
   │      1ms        5ms       20ms
```

每个环节标注：
1. **时延类型**：Tt（传输）、Tp（处理）、Tq（排队）、Ts（调度）、Tproto（协议）
2. **最小/典型/最大**值
3. **是否硬编码/可配置**
4. **优化方向**

### 检查清单

- [ ] **传输时延 Tt**：数据量 / 波特率 是否算过？大帧传输是否超过 10ms？
- [ ] **处理时延 Tp**：是否有 O(n²) 算法？是否有阻塞 I/O（如 `tcdrain`）？
- [ ] **排队时延 Tq**：HandlerThread / 消息队列是否有界？burst 时是否积压？
- [ ] **调度时延 Ts**：`select/poll/epoll` 的超时是多少？是否过长？
- [ ] **协议时延 Tproto**：重试次数、退避策略、握手次数是否合理？
- [ ] **端到端总计**：Σ各环节 <= 产品 KPI？
- [ ] **最坏情况**：最大时延出现时，用户体验是否可接受？
- [ ] **资源竞争**：高并发时，锁竞争会让典型值恶化多少？

### 常见陷阱
| 陷阱 | 症状 | 检查方法 |
|------|------|---------|
| `select(timeout=50ms)` 轮询小帧 | 帧检测延迟 0~50ms | 计算 数据量/波特率，若远小于 select 超时，则 select 是瓶颈 |
| `tcdrain` 每次写后都调用 | 发送线程阻塞 | 评估是否可以移除或异步化 |
| OTA 中硬编码 `sleep(200ms)` | 传输时间被 sleep 主导 | 用总时间反推，若 sleep 占 80% 则优化空间巨大 |
| Binder 大对象传输 | `TransactionTooLargeException` | AIDL 接口中限制数组大小 |
| MessageQueue 无界累积 | 内存增长，延迟递增 | 加队列深度监控或限制 |

### 产出模板
```markdown
### 时延预算：MCU 上行帧 → App UI

| 环节 | 类型 | 最小 | 典型 | 最大 | 优化方向 |
|------|------|------|------|------|---------|
| UART 传输 | Tt | 0.2ms | 0.2ms | 36ms | 升级波特率 |
| C select | Ts | 0 | 25ms | 50ms | 降低 timeout |
| C parser | Tp | 0.1ms | 0.5ms | 2ms | - |
| JNI 回调 | Tp | 0.1ms | 0.2ms | 6ms | 避免首次 Attach |
| Handler 排队 | Tq | 0 | 0 | ∞ | 监控队列深度 |
| Feature 解析 | Tp | 0.2ms | 0.5ms | 2ms | - |
| Binder IPC | Ts | 1ms | 3ms | 20ms | 减少 listener |
| App 处理 | Tp | 1ms | 3ms | 10ms | 不可控 |
| **总计** | - | **~3ms** | **~32ms** | **~126ms** | - |

- **KPI**：< 50ms
- **达标情况**：典型值达标，最大值超标（需优化 select 和 Binder）
```

---

## 附录 A：审查流程 SOP

### 场景 1：新增功能（如新增胎压传感器上报）

1. **作者自审**（30 分钟）：
   - 维度 1（数据流图）：画出新数据通路
   - 维度 3（状态机）：如果有新状态（如传感器校准中）
   - 维度 4（决策树）：新增 AIDL 方法的分支覆盖

2. **Reviewer 审查**（20 分钟）：
   - 维度 2（时序图）：检查多线程安全
   - 维度 5（资源所有权）：检查 JNI 数组和引用
   - 维度 7（跨层契约）：检查新命令码 Java/C 一致性

3. **发布前回归**（10 分钟）：
   - 维度 6（故障传播）：模拟传感器断开，看日志和错误码
   - 维度 8（时延预算）：确认上报频率不冲击 FrameRouter 队列

### 场景 2：性能优化（如提升 OTA 速度）

1. 维度 8（时延预算）：拆解当前瓶颈，量化优化目标
2. 维度 2（时序图）：确认修改线程模型后无新竞态
3. 维度 6（故障传播）：确认 pacing 修改后，MCU 端不会丢包
4. 维度 5（资源所有权）：确认新增 buffer 有无泄漏

### 场景 3：偶发 Bug 根因分析

1. 维度 6（故障传播）：从现象反推到根因层
2. 维度 2（时序图）：画出崩溃/卡死时的线程状态
3. 维度 5（资源所有权）：检查是否有 fd/内存泄漏
4. 维度 3（状态机）：检查异常输入是否导致非法状态

---

## 附录 B：Checklist 汇总表（打印版）

```
□ 维度1 数据流
  □ 每段数据格式标注清楚
  □ 拷贝点明确，无遗漏拷贝
  □ 线程切换点无耗时阻塞
  □ 大小端、符号扩展正确

□ 维度2 时序
  □ 锁范围最小化
  □ 锁顺序全局一致（防死锁）
  □ 异常路径锁释放（try-finally）
  □ volatile / Atomic 使用正确

□ 维度3 状态机
  □ 状态全覆盖，有 default
  □ 每个状态有超时兜底
  □ 非法转换有处理（非静默）
  □ 转换失败副作用回滚

□ 维度4 决策树
  □ 所有分支有返回值
  □ 错误码一码一事
  □ 异常路径资源释放
  □ 空指针/空数组有处理

□ 维度5 资源所有权
  □ malloc ↔ free 配对
  □ open ↔ close 配对
  □ NewGlobalRef ↔ DeleteGlobalRef 配对
  □ Get/Release ByteArrayElements 配对
  □ register ↔ unregister 配对

□ 维度6 故障传播
  □ 底层故障能被最早层发现
  □ 错误码逐层保留（至少打 log）
  □ 重试有上限和退避
  □ 有降级策略
  □ 故障恢复后自动恢复

□ 维度7 跨层契约
  □ Java/C 常量值一致
  □ 长度限制 Java <= C（或相等）
  □ Java 超时 >= C 内部最大耗时
  □ 枚举/错误码一一对应
  □ AIDL 版本兼容性

□ 维度8 时延预算
  □ 传输时延 = 数据量/波特率
  □ select/epoll 超时不过大
  □ 无硬编码 sleep 主导总时间
  □ 端到端 <= KPI
  □ 最坏情况可接受

□ 附录C C语言陷阱
  □ 无符号运算未溢出（uint16_t * uint16_t 先提升为 size_t）
  □ memcpy 目标缓冲区 >= 源大小
  □ 每个 lock 后所有 exit path 都 unlock
  □ switch 有 default 分支
  □ volatile 不替代 atomic（C11 用 stdatomic.h）
  □ free 后立即置指针为 NULL

□ 附录D Java陷阱
  □ 匿名内部类/Lambda 不长期持有 Activity/Context
  □ 时间戳用 long，无 int 截断
  □ catch 块至少打 warning 日志
  □ CopyOnWriteArrayList 写频率低
  □ synchronized 块不调用 Binder IPC

□ 附录E JNI陷阱
  □ NewGlobalRef / DeleteGlobalRef 配对
  □ GetByteArrayElements / Release... mode 正确
  □ Call*Method 后 ExceptionCheck
  □ 字符串用 Modified UTF-8，避免 \u0000
  □ 局部引用表不溢出（循环中及时 DeleteLocalRef）
```

---

## 附录 C：C 语言专项陷阱速查

### C1. 有符号 / 无符号混用（Signness Bugs）

**为什么容易出错**：C 的隐式类型转换规则极其复杂，`uint8_t` 提升为 `int` 时如果最高位是 1，可能变成负数。

**典型场景**：
```c
uint16_t a = 65535, b = 65535;
size_t c = a * b;  // BUG! a*b 先提升为 int，溢出为未定义行为
// 正确：size_t c = (size_t)a * (size_t)b;
```

**在 MCU 项目中的体现**：
- `mcu_upgrade.c` 中 `offset = (size_t)index * (size_t)pack_len;` 当前正确，但如果 `(size_t)` 强制转换遗漏，就有 UB 风险。
- `FrameRouter.java` 收到 `byte b = payload[0]; int v = b;` 如果没有 `& 0xFF`，`v` 会变成负数。

**检查方法**：编译时开 `-Wsign-conversion -Wsign-compare`。

---

### C2. 整数溢出（Integer Overflow）

**典型场景**：
```c
uint32_t rate = (bytes_done * 1000) / elapsed_ms;
// 如果 bytes_done > 4MB，bytes_done * 1000 溢出 32 位
```

**在 MCU 项目中的体现**：
- `mcu_upgrade.c` 中 `rate_bps = (uint32_t)(((uint64_t)bytes_done * 1000ULL) / elapsed_ms);` 当前用 `uint64_t` 过渡，正确。但如果省略 `1000ULL`，就有溢出风险。

**检查方法**：编译时开 `-ftrapv`，或用 Clang UBSan。

---

### C3. 缓冲区溢出（Buffer Overrun）

**典型场景**：
```c
uint8_t frame[MCU_MAX_FRAME_LEN];
size_t frame_len = mcu_build_frame_bytes(frame, sizeof(frame), ...);  // 安全
// 但如果 frame 改为指针：
uint8_t *frame = malloc(MCU_MAX_FRAME_LEN);
frame_len = mcu_build_frame_bytes(frame, sizeof(frame), ...);  // BUG! sizeof(指针)=8
```

**检查方法**：AddressSanitizer (`-fsanitize=address`)，或静态分析工具。

---

### C4. 未定义行为（UB）—— 左移溢出、有符号右移

**典型场景**：
```c
int x = -1;
int y = x << 1;   // UB! 有符号负数左移
int z = x >> 1;   // 实现定义行为（算术右移还是逻辑右移？）
```

**在 MCU 项目中的体现**：
- 协议解析中大量位运算，确保操作数是无符号类型：`uint32_t` 而非 `int`。

**检查方法**：`-fsanitize=undefined`。

---

### C5. 竞争条件之外的 TOCTOU（Time-of-Check to Time-of-Use）

**典型场景**：
```c
if (ctx->state.link_state == LINK_LINKED) {   // 检查
    pthread_mutex_lock(&ctx->lock);
    send_data(...);                             // 使用时状态已变
    pthread_mutex_unlock(&ctx->lock);
}
```

**正确做法**：检查和操作必须在同一把锁内完成。

---

### C6. 宏定义的副作用

**典型场景**：
```c
#define MIN(a,b) ((a) < (b) ? (a) : (b))
int x = MIN(i++, j++);  // i 或 j 会被递增两次
```

**建议**：复杂宏改为 `static inline` 函数。

---

### C7. `volatile` 误用

**为什么容易出错**：`volatile` 不保证原子性，也不保证内存顺序（无内存屏障）。

**在 MCU 项目中的体现**：
```c
static volatile bool g_upgrade_abort_requested = false;
```

严格来说应该用 `stdatomic.h`：
```c
#include <stdatomic.h>
static _Atomic bool g_upgrade_abort_requested = false;
```

---

### C8. 信号安全函数（Signal Safety）

**为什么容易出错**：信号 handler 中只能调用 async-signal-safe 函数（`write`、`_exit` 等），不能调用 `malloc`、`printf`、`pthread_mutex_lock`。

**在 MCU 项目中的体现**：QSJNIC 目前没有信号 handler。如果未来增加 `SIGPIPE` 或 `SIGALRM` 处理，务必遵守此约束。

---

## 附录 D：Java 语言专项陷阱速查

### D1. 匿名内部类隐式持有外部引用（内存泄漏）

**为什么容易出错**：Java 匿名类和 Lambda 会持有 enclosing 实例的引用。

**典型场景**：
```java
// McuBinder 中
frameListeners.register(new IMcuFrameListener.Stub() {
    @Override
    public void onMcuFrame(...) {
        // 这个匿名 Stub 持有 McuBinder 的引用
    }
});
```

如果 `McuBinder` 通过某种方式（如传入的 Context）间接持有 Activity，且 `unregister` 不及时，Activity 泄漏。

**检查方法**：LeakCanary。

---

### D2. 异常静默吞没（Exception Swallowing）

**典型场景**：
```java
catch (RemoteException ignored) {}
```

**在 MCU 项目中的体现**：`VehicleFeature.broadcastTyped`、`RadioFeature.broadcastTyped` 等多处 `catch (RemoteException ignored)`。App 进程死亡时没有任何日志，调试困难。

**建议**：至少打 warning log：
```java
catch (RemoteException e) {
    Log.w(TAG, "listener dead", e);
}
```

---

### D3. 时间计算溢出与 `int` 截断

**典型场景**：
```java
int timeout = 1500;
long deadline = SystemClock.uptimeMillis() + timeout;  // 安全（int 提升为 long）
// 但反过来：
int deadline = (int)(SystemClock.uptimeMillis() + 1500);  // BUG! 截断
```

**建议**：所有时间戳、deadline 一律用 `long`。

---

### D4. `NullPointerException` 的链式调用

**典型场景**：
```java
snapshot.mcuVersion.hwVersion = Decoders.parseMcuVersion(body);
```

如果 `snapshot.mcuVersion` 被其他线程设为 null（虽然这里加了 `synchronized`），会 NPE。

**建议**：关键路径用 `Objects.requireNonNull` 或提前检查。

---

### D5. `CopyOnWriteArrayList` 的误用

**为什么容易出错**：写操作会复制整个数组，写频率高时性能极差。

**在 MCU 项目中的体现**：`DomainBus.features` 是 `CopyOnWriteArrayList`，但 `register` 只在 onCreate 时调用几次，遍历（`onFrame`）非常频繁。**这是正确使用场景**。

**反例**：如果有人在每帧都 `register/unregister`，性能会暴跌。

---

### D6. `synchronized` 与 `ReentrantLock` 的选择

**为什么容易出错**：`synchronized` 不响应中断，也不能超时。

**在 MCU 项目中的体现**：`VehicleFeature.snapshot()` 用 `synchronized(snapshot)`。如果某线程死锁，其他线程永远等待。

**建议**：关键路径用 `ReentrantLock.tryLock(timeout)` 替代裸 `synchronized`。

---

## 附录 E：JNI 边界专项陷阱速查

### E1. UTF-8 / Modified UTF-8 混淆

**为什么容易出错**：JNI 的 `GetStringUTFChars` 返回的是 **Modified UTF-8**（`\u0000` 编码为 `0xC0 0x80`），不是标准 UTF-8。

**在 MCU 项目中的体现**：
```cpp
const char *port = env->GetStringUTFChars(jPort, nullptr);
```
如果 `jPort` 包含 `\u0000`，C 层的 `strlen`、`open()` 会提前截断。

**建议**：避免在 JNI 字符串路径中包含 `\u0000`；如需传递二进制数据，用 `byte[]` 而非 `String`。

---

### E2. 数组拷贝方向与 `ReleaseByteArrayElements` 的 mode

**为什么容易出错**：`GetByteArrayElements` 返回的指针可能是直接映射也可能是拷贝，mode 参数决定写回行为。

**在 MCU 项目中的体现**：
```cpp
jbyte *bytes = env->GetByteArrayElements(jPayload, nullptr);
env->ReleaseByteArrayElements(jPayload, bytes, JNI_ABORT);
```

✅ 当前是只读，`JNI_ABORT` 正确。但如果未来 C 层修改了 `bytes[i]` 而 mode 仍是 `JNI_ABORT`，修改会丢失。

**建议**：加注释说明 `JNI_ABORT` 的原因；如果 C 层需要写回，mode 改为 `0`。

---

### E3. 局部引用表溢出（Local Reference Table Overflow）

**为什么容易出错**：JNI 方法中创建的局部引用在函数返回前不会自动释放。循环中大量创建会溢出。

**在 MCU 项目中的体现**：
```cpp
// jni_notify_cb 每帧调用
jbyteArray jPayload = env->NewByteArray((jsize)body_len);
env->DeleteLocalRef(jPayload);  // ✅ 正确
```

**但如果未来批量处理**：
```cpp
for (int i = 0; i < 1000; i++) {
    jobject obj = env->NewObject(...);  // 没有 DeleteLocalRef!
}
```

**检查方法**：Android `adb shell setprop debug.checkjni 1` 开启 CheckJNI。

---

### E4. 异常未清除（Pending Exception）

**为什么容易出错**：JNI 中 Java 异常 pending 时，继续调用 JNI 函数会导致崩溃或未定义行为。

**在 MCU 项目中的体现**：
```cpp
env->CallStaticVoidMethod(g_router_class, g_router_on_frame, ...);
if (env->ExceptionCheck()) {
    env->ExceptionDescribe();
    env->ExceptionClear();
}
```

✅ 这里检查了异常。但如果 `CallStaticVoidMethod` 和 `ExceptionCheck` 之间加了其他 JNI 调用，那些调用会失败。

**建议**：`Call*Method` 后立即 `ExceptionCheck`，中间不插入其他 JNI 调用。

---

### E5. 直接缓冲区（Direct ByteBuffer）的生命周期

**为什么容易出错**：`NewDirectByteBuffer` 创建的 Buffer，C 层持有指针，Java 层如果 GC 了 Buffer 对象，C 层指针悬空。

**在 MCU 项目中的体现**：QSJNIC 目前没有使用 `DirectByteBuffer`。但如果未来为了零拷贝引入：
```cpp
jobject buffer = env->NewDirectByteBuffer(c_ptr, size);
```

Java 层必须长期持有 `buffer` 引用（如全局引用），否则 GC 后 `c_ptr` 悬空。

---

### E6. `FindClass` 的类加载器上下文

**为什么容易出错**：`FindClass` 使用当前线程的类加载器。如果在非 Java 线程（如 C reader 线程）中首次调用 `FindClass`，可能找不到应用类（因为系统类加载器不认识应用类）。

**在 MCU 项目中的体现**：`jni_bridge.cpp` 的 `JNI_OnLoad` 中在主线程调用了 `FindClass`，并把结果缓存到 `GlobalRef`。这是正确做法。

**反例**：如果 `jni_notify_cb` 中实时 `FindClass`，在 reader 线程（attach 后上下文类加载器为系统类加载器）会失败。

**建议**：所有 `FindClass` 都在 `JNI_OnLoad` 中完成，结果缓存为 `GlobalRef`。

---

*框架版本：v2.0*
*适用项目：MCU 通信服务 + JNI + Android Service + AIDL*
*生成时间：2026-06-04*
