别再让业务层裸奔 CarPropertyManager 了!谈谈汽车车载核心服务的架构封装

1 阅读5分钟

前言

Android Automotive 项目中,CarPropertyManager 几乎是访问一切车辆状态的入口。但在真实项目里,如果直接在 ViewModel 或业务层大量使用它,代码会变得难以维护、难以测试,也完全不符合 现代Android 架构 的分层思想。

本文将从零开始,梳理一套基于 Clean Architecture 的 CarPropertyManager 封装方案,涵盖:

  • 为什么不能直接用
  • 如何用抽象基类统一封装
  • callbackFlow 的正确姿势
  • Flowsuspend 如何取舍
  • Repository 的真正职责边界

1. 什么是 CarPropertyManager?

1.1 基本概念

CarPropertyManager 是 Android Automotive OS 提供的车辆属性访问入口,底层通过与 VHAL(Vehicle Hardware Abstraction Layer) 交互,读取或监听车辆的实时状态。

常见可访问的属性包括:

属性说明
PERF_VEHICLE_SPEED车速
GEAR_SELECTION档位
EV_BATTERY_LEVEL电池电量(SOC)
ENV_OUTSIDE_TEMPERATURE室外温度
DOOR_OPEN车门状态
INFO_VIN车辆 VIN 码
INFO_MODEL车辆型号

1.2 基础用法

读取单次属性值:

image.png

注册监听,持续接收变化:

image.png

取消监听:

image.png

看起来不复杂,问题在哪里?


2. 为什么不要在业务层直接使用 CarPropertyManager?

2.1 反例

很多项目一开始会写出这样的代码:

image.png

2.2 这样写有哪些问题?

① UI 层与 Framework 强耦合

CarPropertyManager 是 Automotive Framework API,ViewModel 不应该感知它的存在。一旦底层 API 变动,ViewModel 就得跟着改。

② 单元测试困难

CarPropertyManager 无法在 JUnit 环境中直接实例化,测试 ViewModel 需要大量 Mock,成本极高。

③ 生命周期管理散乱

registerCallbackunregisterCallback 分散在各处,极易出现忘记注销、重复注册或内存泄漏的问题。

④ 属性映射逻辑到处都是

每个属性的类型转换(value as Floatvalue as Int)散落在不同地方,没有统一的错误处理。

⑤ 大量重复代码

每个属性都要重写一套几乎相同的 registerCallbackcallbackunregisterCallback 流程,毫无复用。

💡 我们需要一个统一的数据访问层,把 CarPropertyManager 的使用细节彻底封装起来。


3. BaseCarPropertySource:统一封装的抽象基类

3.1 为什么用抽象基类?

观察所有车辆属性的访问流程,会发现结构完全一致:

image.png

唯一变化的只有两点:

  • propertyId:访问哪个属性
  • mapValue:如何把原始值转成业务类型

这正是模板方法模式的最佳使用场景——稳定的流程放在基类,变化的部分留给子类实现。

3.2 基类设计

image.png

3.3 子类只需关心两件事

image.png

整个注册、监听、注销的细节,子类完全无需关心。


4. callbackFlow:将 Callback 正确转换成 Flow

4.1 为什么选 callbackFlow?

很多人会用 MutableSharedFlow + 手动管理生命周期的方式:

image.png

这种做法的问题在于:

  • 注册和取消的时机不受 Flow 订阅者控制
  • 无订阅者时仍在消耗资源
  • GlobalScope 容易造成泄漏

callbackFlow 的优势:

特性callbackFlow
订阅时才注册
无订阅者自动取消注册
生命周期与 collector 绑定
背压处理✅(trySend / buffer

4.2 高频属性的背压问题

⚠️ 注意:对于车速这类高频更新的属性,在低端硬件上可能出现 Flow 积压。

可以根据场景选择策略:

image.png


5. 静态属性为什么更适合 suspend,而不是 Flow

5.1 API 的命名应该符合实际行为

这类属性的正确表达方式是 suspend 函数:

// ✅ 推荐
suspend fun getVin(): String
suspend fun getInfoModel(): String
suspend fun getManufacturer(): String

判断原则:

  • 数据持续变化,需要持续监听 → Flow
  • 数据一次性读取,几乎不变化 → suspend fun

8. 完整架构总览

经过以上各层的设计,整体架构如下:

         CarPropertyManager
                 │
                 ▼
   BaseCarPropertySource<T>
                 │
    ┌────────────┴────────────┐
    │                         │
    ▼                         ▼
OutsideTemperatureSource   InfoModelSource
(observe: Flow<Float>)   (get: suspend String)
    │                         │
    └────────────┬────────────┘
                 ▼
          CarRepository
          (combine / map)
                 │
                 ▼
          DashboardInfo
          (领域模型)
                 │
                 ▼
           ViewModel
          (StateFlow)
                 │
                 ▼
           Compose UI

每一层的职责边界清晰:

层级职责
CarPropertyManagerFramework API,提供车辆属性访问能力
BaseCarPropertySource封装注册/回调/注销细节,转换为 Flow 或 suspend
Repository聚合多个 Source,提供领域模型
ViewModel持有 UI 状态,处理 UI 事件
Compose UI展示数据,处理格式化

总结

CarPropertyManager 是 Framework 层的访问入口,职责止于"能拿到数据";BaseCarPropertySource 封装访问细节,职责止于"把数据变成 Flow 或值";Repository 聚合业务所需的数据,职责止于"给 ViewModel 提供可直接使用的领域模型"。

好的架构设计,本质上是在做一件事:让每一层只承担它应该承担的职责,不多也不少。

Screen_recording_20260613_185226.gif

下图是我设置外部温度的截图,上面的动图能看到变化.

image.png

代码

GitHub

参考资料