04 · 超时监控与回调机制

源码:SettingsCarPropertyManager.kt 行 95-201, 306-319, 530-609, 658-692

SCPM 区别于「直接调 AOSP」的三个核心增值机制:超时监控回调双层分发缓存策略

1. 超时监控状态机

下发信号后,车端应在一定时间内回弹确认。SCPM 用一个主线程 Handler 实现完整的等待→回弹/超时流转。

1.1 核心代码位置

函数行号作用
startTimeOutMonitor530-539入口,BATCH_PROP 遍历集合
startTimeOutMonitorReal541-562真正启动计时
processTimeout155-189车端回弹时的决策
handlePropertyTimeout191-201真超时处理
getTimeoutMsgId568-570msgId = (propId#areaId).hashCode()
isInHMITriggerWaitingDuration564-566查是否还在等待(hasMessages)

1.2 状态流转图

stateDiagram-v2
    [*] --> 下发信号: setCarProperty
    下发信号 --> 启动判断: startTimeOutMonitorReal

    state 启动判断 <<choice>>
    启动判断 --> 不计时: propId 不在 observedSet
    启动判断 --> 不计时: timeoutValue == TIMEOUT_IGNORE
    启动判断 --> 等待回弹: 满足条件 sendMessageDelayed

    等待回弹 --> 等待回弹: 车端回弹 CONTINUE
    等待回弹 --> 等待回弹: 车端回弹 RESTART 重启计时
    等待回弹 --> [*]: 车端回弹 REMOVE(默认) 取消计时
    等待回弹 --> 真超时: 延时到期

    真超时 --> onSetPropertyTimeout: 回调业务层
    真超时 --> 值比对: 下发值 vs 当前缓存值
    值比对 --> [*]: 一致
    值比对 --> onDataVerifyFailed: 不一致(数据校验失败)

    不计时 --> [*]

1.3 关键规则

  1. 只对关心的信号计时startTimeOutMonitorReal (546) 检查 mObservedPropertyIdSet.contains(timeoutPropId),没注册的信号不计时——因为超时也不会回弹,计时无意义。
  2. 下行/上行信号映射VehiclePropertyConfig.getMappingTimeoutPropId(propId) (542) 处理「下发 A 信号,回弹看 B 信号」的场景。
  3. timeoutValue 可按值定制:同一个 propId 不同 value 可以有不同超时(如开到不同高度耗时不同),getCustomTimeoutValue(propId, value)
  4. 三态 Flag:业务层通过 getTimeoutFlag(value) (674 调用) 决定回弹后的行为——CONTINUE(继续等,回弹的不是终态)、RESTART(重新计时)、默认 REMOVE(取消计时)。

2. 回调双层结构 + 事件分发

2.1 双层结构

graph TD
    subgraph 下层["下层:唯一底层 dispatcher"]
        A["SettingsCarPropertyEventCallback<br/>(内部类, implements AOSP CarPropertyEventCallback)"]
        A1["onChangeEvent :662"]
        A2["onErrorEvent :687"]
    end

    subgraph 内部["内部处理链"]
        B1["mockErrorEvent 拦截 :664"]
        B2["mockTimeoutEvent 拦截 :669"]
        B3["cachePropVal 缓存 :673"]
        B4["processTimeout 超时决策 :674"]
        B5{"propId in observedSet?"}
    end

    subgraph 上层["上层:业务层回调"]
        C1["onPropertyChanged :680"]
        C2["onSetPropertyError :690"]
        C3["onSetPropertyTimeout :194"]
        C4["onDataVerifyFailed :199"]
        C5["onPropertyManagerPrepared :311"]
        C6["onPropertyManagerReleased :315"]
    end

    CarEvent["车端 CarPropertyEvent"] --> A
    A --> A1
    A --> A2
    A1 --> B1
    B1 -->|mock error| A2
    B1 -->|正常| B2
    B2 -->|mock timeout| Drop["直接返回"]
    B2 -->|正常| B3
    B3 --> B4
    B4 --> B5
    B5 -->|是| C1
    B5 -->|否| Warn["打 w 日志 :682"]
    A2 --> C2
    B4 -.->|真超时| C3
    B4 -.->|下发值!=缓存值| C4
    CarService["registerCarServiceListener :308"] --> C5
    CarService --> C6

2.2 onChangeEvent 处理顺序(662-684)

1. mockErrorEvent?  → 是:转 onErrorEvent,return
2. mockTimeoutEvent? → 是:直接 return(吞掉)
3. cachePropVal      → 更新缓存
4. processTimeout    → 按 CarPropertyTimeoutFlag 处理计时
5. observedSet 过滤   → 在集合内:onPropertyChanged 上抛
                      → 不在:打 w 日志("un-excepted observe propId")

⚠️ 第 5 步是常见坑:如果业务层忘了 registerPropertyCallback(ids) 把 propId 加进 observedSet,即使底层有事件,onPropertyChanged不会被回调,只在日志里看到 w 级警告。这是「为什么我 set 了也 get 了,但收不到变化通知」的常见原因之一。

2.3 连接监听(306-319)

setCarManagerCallback 在注册业务回调的同时,向 VehicleControlManager 注册了 OnCarServiceListener

  • onCarServiceManagerPreparedmCallback.onPropertyManagerPrepared()(业务层通常在此 registerPropertyCallback + 刷新 UI)
  • onCarServiceManagerReleasedmCallback.onPropertyManagerReleased() + onServiceDisconnect()(清缓存、清 handler 消息)

3. 缓存策略

3.1 核心位置

组件行号作用
mPropertiesCache95HashMap<String, CarPropertyValue<*>>
getCacheKey576-578"propId#areaId"
getCachedProp580-584
cachePropVal586-609
remoteGetProperty391-405远端获取 + 缓存 + 隐式注册
getProperty371-378优先缓存
getPropertyWithoutCache381-386强制远端

3.2 写缓存决策(cachePropVal 586-609)

flowchart TD
    In["输入 CarPropertyValue"] --> Key["key = propId#areaId"]
    Key --> First{"首次缓存?<br/>cache[key]==null"}
    First -->|是| FirstVal["getPropValueForFirstCache(value)"]
    FirstVal --> HasInit{"有初始终态值?"}
    HasInit -->|是| StoreInit["缓存初始终态值<br/>直接 return"]
    HasInit -->|否| Ignore{"业务层 isIgnoreCache?"}
    First -->|否| Ignore
    Ignore -->|true| Skip["跳过,不缓存"]
    Ignore -->|false| Store["cache[key] = value"]

3.3 读缓存决策(getProperty 371-378)

flowchart TD
    Get["getProperty propId, areaId"] --> TryCache["getCachedProp"]
    TryCache --> Hit{"缓存命中?"}
    Hit -->|是| Return["返回缓存值"]
    Hit -->|否| Remote["remoteGetProperty"]
    Remote --> Conn{"isConnected?"}
    Conn -->|否| Null["返回 null"]
    Conn -->|是| Fetch["VCM.getProperty → AOSP"]
    Fetch --> DoCache["cachePropVal 缓存"]
    DoCache --> AutoReg{"propId 在 observedSet?"}
    AutoReg -->|否| Reg["自动注册 callback<br/>加入 NoNeedObservedSet"]
    AutoReg -->|是| Done
    Reg --> Done["返回值"]

3.4 三个关键设计

  1. key = propId#areaId:同一属性的不同区域(如左前门/右后门)各自独立缓存,互不污染。
  2. 首次缓存注入终态值getPropValueForFirstCache (590-597) 让 UI 首次展示时拿到的是「终态值」而非中间值(避免 UI 抖动)。这对 JIRA 1839 的车门闪烁场景至关重要——但前提是首次缓存前有调过 getProperty,而 DoorUnLockActivity 恰恰没预读,所以没吃到这个兜底。
  3. 业务层可控isIgnoreCache(value) (605) 让业务层决定特定值是否进缓存(如过渡态值不缓存)。

附:三个机制的协作关系

graph LR
    Write["写 setCarProperty"] --> Timeout["启动超时计时"]
    Timeout --> Wait["等待回弹"]
    Car["车端回弹"] --> Dispatcher["底层 dispatcher onChangeEvent"]
    Dispatcher --> Cache["更新缓存"]
    Dispatcher --> TimeoutProc["处理超时 Flag"]
    TimeoutProc -->|REMOVE| Cancel["取消计时"]
    TimeoutProc -->|CONTINUE| Wait
    TimeoutProc -->|RESTART| Timeout
    Dispatcher --> Notify["onPropertyChanged 上抛"]
    Wait -->|超时| Fail["onSetPropertyTimeout + onDataVerifyFailed"]

    Read["读 getProperty"] -.->|优先| Cache
    Cache -.->|未命中| Remote["remoteGetProperty 隐式注册"]
    Remote -.-> Dispatcher

← 返回 00-README.md | 业务实战见 座舱车门知识库