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

前言

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

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

  • 为什么不能直接用
  • 如何用抽象基类统一封装
  • callbackFlow 的正确姿势
  • Flow 与 suspend 如何取舍
  • 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 基础用法

读取单次属性值:

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

取消监听:

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


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

2.1 反例

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

2.2 这样写有哪些问题?

① UI 层与 Framework 强耦合

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

② 单元测试困难

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

③ 生命周期管理散乱

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

④ 属性映射逻辑到处都是

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

⑤ 大量重复代码

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

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


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

3.1 为什么用抽象基类?

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

唯一变化的只有两点:

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

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

3.2 基类设计

3.3 子类只需关心两件事

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


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

4.1 为什么选 callbackFlow?

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

这种做法的问题在于:

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

callbackFlow 的优势:

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

4.2 高频属性的背压问题

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

可以根据场景选择策略:


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

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

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

kotlin 复制代码
// ✅ 推荐
suspend fun getVin(): String
suspend fun getInfoModel(): String
suspend fun getManufacturer(): String

判断原则:

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

8. 完整架构总览

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

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

每一层的职责边界清晰:

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

总结

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

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

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

代码

GitHub

参考资料

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen5 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone5 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui