一篇讲透 Android Car CarProperty 车辆信号链路(保姆级 / 零基础)
适用对象:刚接触车机 Android(AAOS)开发的同学。 目标:把"一个 App 怎么读到车速、怎么控制车窗"这件事,从最上面的应用一路追到最底层的 VHAL,每一层是哪个类、调了哪个方法,全部讲清楚。 本文所有类名、方法名、行号均来自 AOSP
packages/services/Car实际源码,可对照阅读。
0. 先用大白话理解:CarProperty 到底是什么?
车上有几千个"数据点":车速、油量、空调温度、车窗开度、档位、车门状态…… 在 Android 车机里,这些数据点被统一抽象成一个东西,叫 Property(属性)。
你可以把它想象成一张巨大的 Excel 表:
| 列名 | 含义 | 举例 |
|---|---|---|
| propertyId | 这是"哪一个"信号 | PERF_VEHICLE_SPEED(车速) |
| areaId | 这是"哪个位置"的信号 | 左前门 / 右后门 |
| value | 当前的值 | 60.0(km/h) |
| status | 这个值能不能信 | AVAILABLE / UNAVAILABLE |
| timestamp | 这个值是什么时候的 | 开机以来的纳秒数 |
App 想干的事其实就三件:
- 读一次(getProperty)——"现在车速多少?"
- 写一次(setProperty)——"把主驾车窗降下来"
- 订阅(registerCallback)——"车速一变就通知我"
整篇文章,就是讲这三件事在系统里是怎么一层层往下传、又怎么一层层传回来的。
1. 全景图:一张图看懂 5 层架构
┌─────────────────────────────────────────────────────────────┐
│ ① 应用层 App │
│ 你的 Activity / Service │
│ 拿到 CarPropertyManager,调 get/set/registerCallback │
└───────────────────────────┬─────────────────────────────────┘
│ 普通 Java 方法调用
┌───────────────────────────▼─────────────────────────────────┐
│ ② Car SDK 层 (car-lib,跑在你 App 进程里) │
│ CarPropertyManager │
└───────────────────────────┬─────────────────────────────────┘
│ Binder 跨进程 (ICarProperty.aidl)
┌───────────────────────────▼─────────────────────────────────┐
│ ③ CarService 层 (com.android.car 进程 / system_server 旁) │
│ CarPropertyService ←权限校验、客户端管理 │
│ │ │
│ PropertyHalService ←ID 转换、数据格式转换 │
└───────────────────────────┬─────────────────────────────────┘
│ Java 方法 → VehicleStub
┌───────────────────────────▼─────────────────────────────────┐
│ ④ VHAL 客户端胶水层 │
│ VehicleHal → VehicleStub → AidlVehicleStub │
└───────────────────────────┬─────────────────────────────────┘
│ Binder 跨进程 (IVehicle.aidl, HIDL/AIDL HAL)
┌───────────────────────────▼─────────────────────────────────┐
│ ⑤ VHAL 服务端 + 车身网络 (厂商实现,C++) │
│ IVehicle 实现 → CAN/LIN/以太网 → MCU → 真正的车 │
└─────────────────────────────────────────────────────────────┘
记住一句话: 上面两层(①②)和你的 App 在同一个进程;从第 ③ 层开始就跨进程了,靠 Binder 通信;第 ⑤ 层是厂商的 C++ 代码,已经贴着硬件了。
2. 准备工作:App 怎么拿到 CarPropertyManager?
App 不能直接 new 一个 Manager,必须先连上 Car 服务,再"要"一个 Manager 出来。
// 1. 创建 Car 对象并连接 CarService
Car car = Car.createCar(context);
// 2. 向 Car 要 PROPERTY_SERVICE 对应的管理器
CarPropertyManager mgr =
(CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE);
类比:
Car就像安卓里的Context,getCarManager()就像getSystemService()。 你要 WiFi 功能就getSystemService(WIFI_SERVICE),要车辆属性就getCarManager(PROPERTY_SERVICE)。
拿到的 CarPropertyManager 内部持有一个 Binder 代理 mService(类型 ICarProperty),这是它能跟 CarService 通信的关键:
car-lib/src/android/car/hardware/property/CarPropertyManager.java:91private final ICarProperty mService;
3. 写方向:setProperty —— "把车窗降下来"
我们以"设置一个属性"为例,一层层往下追。
第 ② 层 · CarPropertyManager.setProperty()
文件:car-lib/src/android/car/hardware/property/CarPropertyManager.java:1963
public <E> void setProperty(Class<E> clazz, int propertyId, int areaId, E val) {
assertPropertyIdIsSupported(propertyId); // ① 先检查这个属性支不支持
runSyncOperation(() -> {
mService.setProperty( // ② 通过 Binder 调到 CarService
new CarPropertyValue<>(propertyId, areaId, val),
mCarPropertyEventToService);
return null;
});
}
这一层做了什么:
- 先确认这个 propertyId 在车上真的存在(
assertPropertyIdIsSupported)。 - 把你的"属性号 + 区域 + 值"打包成一个
CarPropertyValue对象。 - 通过
mService.setProperty(...)这行——注意这里就跨进程了——把请求丢给 CarService。
小白疑问:为什么要
runSyncOperation? 因为 set 操作要等车端返回结果,可能要几百毫秒到几秒,所以这是个同步阻塞调用,官方明确要求不要在主线程调用,否则会卡 UI。
跨进程的桥 · ICarProperty.aidl
文件:car-lib/src/android/car/hardware/property/ICarProperty.aidl:41
interface ICarProperty {
void setProperty(in CarPropertyValue prop, in ICarPropertyEventListener callback) = 4;
CarPropertyValue getProperty(int prop, int zone) = 3;
void registerListener(int propId, float rate, in ICarPropertyEventListener callback) = 0;
...
}
AIDL 是 Android 的"跨进程协议书"。你在 App 进程调
mService.setProperty(), Binder 驱动会把参数序列化,搬到 CarService 进程,再调到服务端的同名方法。 App 这边是"代理(Proxy)",CarService 那边是"实现(Stub)"。
第 ③ 层 · CarPropertyService.setProperty()
文件:service/src/com/android/car/CarPropertyService.java:713
public void setProperty(CarPropertyValue carPropertyValue,
ICarPropertyEventListener listener) {
// ① 校验:propertyId 合法吗?areaId 对吗?值的类型对吗?有写权限吗?
// ② 记录"是谁设置的",方便出错时把错误回调给正确的客户端
mPropertyHalService.setProperty(carPropertyValue); // ③ 往 HAL 层下传
}
这一层是"门卫 + 调度员":
- 校验权限(你这个 App 有没有资格控车窗?没有就抛
SecurityException)。 - 维护一张"哪个客户端订阅了哪个属性"的表(
mPropIdClientMap)。 - 记录"刚才是谁设置的"(
mSetOperationClientMap),这样万一设置失败,能精准地告诉那个 App。 - 真正的活儿交给下一层
PropertyHalService。
第 ③.5 层 · PropertyHalService.setProperty()
文件:service/src/com/android/car/hal/PropertyHalService.java:1047
public void setProperty(CarPropertyValue carPropertyValue) {
HalPropValue valueToSet =
carPropertyValueToHalPropValueLocked(carPropertyValue); // ① 格式转换
mVehicleHal.set(valueToSet); // ② 交给 VehicleHal
}
这一层是"翻译官",干两件最关键的事:
- ID 转换:上层 App 用的 propertyId(叫 manager prop id)和底层 VHAL 用的 propertyId(叫 hal prop id)有时不一样,这里做映射(
halToManagerPropId/managerToHalPropId)。 - 数据结构转换:把面向应用的
CarPropertyValue转成面向硬件的HalPropValue(carPropertyValueToHalPropValueLocked,行号 1701)。
为什么要两套数据结构? 上层
CarPropertyValue是给 Java 应用用的,强类型、好用; 底层HalPropValue贴近 VHAL 的内存布局(int32 数组、float 数组、字节数组),是为了高效跨 HAL 传输。
第 ④ 层 · VehicleHal.set()
文件:service/src/com/android/car/hal/VehicleHal.java:816
public void set(HalPropValue propValue) {
mVehicleStub.set(propValue); // 交给 VehicleStub
}
VehicleHal 是整个 CarService 里唯一跟 VHAL 打交道的总管。它下面是 VehicleStub。
第 ④.5 层 · VehicleStub / AidlVehicleStub
文件:service/src/com/android/car/AidlVehicleStub.java
VehicleStub 是个抽象层,屏蔽了 VHAL 是 AIDL 版还是老的 HIDL 版的差异:
- 新平台用
AidlVehicleStub(对接 AIDL HAL) - 老平台用
HidlVehicleStub
它在这里把 HalPropValue 打包成 SetValueRequests,然后第二次跨进程,调到真正的 VHAL 服务端。
第 ⑤ 层 · IVehicle.setValues() —— 进入厂商地盘
文件:hardware/interfaces/automotive/vehicle/aidl/android/hardware/automotive/vehicle/IVehicle.aidl:155
interface IVehicle {
void getValues(IVehicleCallback callback, in GetValueRequests requests);
void setValues(IVehicleCallback callback, in SetValueRequests requests);
void subscribe(in IVehicleCallback callback, in SubscribeOptions[] options,
int maxSharedMemoryFileCount);
void unsubscribe(in IVehicleCallback callback, in int[] propIds);
...
}
到这里,请求离开了 Google 的框架代码,进入车厂/Tier1 自己写的 C++ VHAL 实现:
IVehicle.setValues() → 厂商 VHAL 解析请求
→ 组一帧 CAN 报文 (或 LIN / 车载以太网)
→ 发给 MCU / 车身控制器
→ 车窗电机转动,玻璃下降 🪟⬇️
注意:
setValues是异步的。VHAL 把命令发上车总线后就返回了, 真正"窗户降到底了"这个结果,是通过后面讲的事件上报通道再传回来的。
✅ 写方向完整链路
App
└─ CarPropertyManager.setProperty() [car-lib]
└─(Binder: ICarProperty)─ CarPropertyService.setProperty() [CarService]
└─ PropertyHalService.setProperty() ←CarPropertyValue→HalPropValue
└─ VehicleHal.set()
└─ VehicleStub/AidlVehicleStub.set()
└─(Binder: IVehicle)─ 厂商 VHAL.setValues() [C++]
└─ CAN/LIN/以太网 → MCU → 车
4. 读方向:getProperty —— "现在车速多少?"
读和写几乎对称,只是数据流反过来,而且读是立刻要结果。
第 ② 层 · CarPropertyManager.getProperty()
文件:car-lib/src/android/car/hardware/property/CarPropertyManager.java:1863
public <E> CarPropertyValue<E> getProperty(int propertyId, int areaId) {
assertPropertyIdIsSupported(propertyId);
CarPropertyValue<E> v = runSyncOperation(() -> {
return mService.getProperty(propertyId, areaId); // Binder → CarService
});
return v;
}
第 ③ 层 · CarPropertyService.getProperty()
文件:service/src/com/android/car/CarPropertyService.java:651
public CarPropertyValue getProperty(int propertyId, int areaId) {
// 校验 + 权限检查后:
return mPropertyHalService.getProperty(propertyId, areaId);
}
第 ③.5 层 · PropertyHalService.getProperty()
文件:service/src/com/android/car/hal/PropertyHalService.java:960
public CarPropertyValue getProperty(int mgrPropId, int areaId) {
int halPropId = managerToHalPropId(mgrPropId); // ID 转换
HalPropValue halPropValue = mVehicleHal.get(halPropId, areaId); // 向下读
return halPropValue.toCarPropertyValue(mgrPropId, ...); // 结果转回 App 格式
}
第 ④ / ⑤ 层 · VehicleHal.get() → IVehicle.getValues()
文件:service/src/com/android/car/hal/VehicleHal.java:689(get(int, int))→ 内部 getValueWithRetry → mVehicleStub.get() → 跨进程 IVehicle.getValues()。
注意 getValueWithRetry(VehicleHal.java:322 附近)有自动重试逻辑:如果车端返回 TRY_AGAIN(数据还没准备好),它会重试几次,省得 App 自己写循环。
✅ 读方向完整链路
App
└─ CarPropertyManager.getProperty()
└─(Binder)─ CarPropertyService.getProperty()
└─ PropertyHalService.getProperty() ←halPropValue.toCarPropertyValue()
└─ VehicleHal.get() → getValueWithRetry()
└─ VehicleStub.get()
└─(Binder)─ 厂商 VHAL.getValues()
5. 订阅 + 事件上报:registerCallback —— "车速一变就告诉我"
这是 CarProperty 最常用、也最容易绕晕的部分。它分两个阶段: 阶段 A 注册(自上而下)、阶段 B 事件回传(自下而上)。
阶段 A:注册订阅
第 ② 层 · CarPropertyManager.registerCallback()
文件:car-lib/src/android/car/hardware/property/CarPropertyManager.java:1193
public boolean registerCallback(CarPropertyEventCallback callback,
int propertyId, float updateRateHz) {
// ① 取出该属性的配置,校验采样率是否合理
float rate = InputSanitizationUtils.sanitizeUpdateRateHz(config, updateRateHz);
// ② 每个 propertyId 用一个"控制器"管理它的所有回调
CarPropertyEventCallbackController controller =
mPropertyIdToCarPropertyEventCallbackController.get(propertyId);
if (controller == null) {
controller = new CarPropertyEventCallbackController(propertyId, mLock, ...);
}
controller.add(callback, rate); // 把你的回调登记进去
...
}
关键角色 CarPropertyEventCallbackController
(car-lib/src/com/android/car/internal/CarPropertyEventCallbackController.java):
它在 App 进程内管理"同一个属性的多个回调",比如 A 模块和 B 模块都订阅了车速,事件来了它负责分发给两个回调,还能按各自的采样率过滤。
采样率 updateRateHz 是什么?
ON_CHANGE属性(如车门开关):值变了才推,采样率无所谓。CONTINUOUS属性(如车速):持续变化,你可以要求"每秒最多给我 10 次",写10f。 设太高费 CPU,设太低反应慢,按需取舍。
第 ③ 层 · CarPropertyService.registerListener()
文件:service/src/com/android/car/CarPropertyService.java:330
public void registerListener(int propertyId, float updateRateHz,
ICarPropertyEventListener listener) {
// 把 (客户端 listener, 属性) 关系记进 mPropIdClientMap
// 只有当新采样率比当前订阅的更高时,才真正下发订阅,避免重复
if (sanitizedUpdateRateHz > mPropertyHalService.getSubscribedUpdateRateHz(propertyId)) {
mPropertyHalService.subscribeProperty(propertyId, sanitizedUpdateRateHz); // 行号 370
}
}
精髓:多个 App 订阅同一个属性,CarService 只会向底层下发一份订阅(取最高采样率),事件来了再扇出给所有 App。这叫订阅聚合,省资源。
第 ③.5 / ④ / ⑤ 层 · 一路订阅到 VHAL
PropertyHalService.subscribeProperty() [PropertyHalService.java:1063]
└─ VehicleHal.subscribeProperty() [VehicleHal.java:503/517]
└─ VehicleStub.SubscriptionClient.subscribe()
└─(Binder)─ IVehicle.subscribe() [IVehicle.aidl:227]
└─ 厂商 VHAL 开始监听这个信号
阶段 B:事件回传—— 真正的"信号链路"
车速变了,VHAL 怎么把这个消息送回到你的回调里?这是一条完全反向的路。
第 ⑤ → ④ 层 · VHAL 回调 onPropertyEvent
VHAL 通过当初注册的 IVehicleCallback,调用 onPropertyEvent,事件进入 CarService 进程:
文件:service/src/com/android/car/hal/VehicleHal.java:858
public void onPropertyEvent(ArrayList<HalPropValue> propValues) {
mHandler.post(() -> handleOnPropertyEvent(propValues)); // 切到 Handler 线程处理
}
为什么要
mHandler.post? 因为这是从 Binder 线程进来的,赶紧把活儿丢到自己的工作线程,别堵住 Binder 线程池。
第 ③.5 层 · PropertyHalService.onHalEvents()
文件:service/src/com/android/car/hal/PropertyHalService.java:1284
public void onHalEvents(List<HalPropValue> halPropValues) {
List<CarPropertyEvent> eventsToDispatch = new ArrayList<>();
for (HalPropValue halPropValue : halPropValues) {
int mgrPropId = halToManagerPropId(halPropId); // ① ID 转回 App 用的
CarPropertyValue<?> carPropertyValue =
halPropValue.toCarPropertyValue(mgrPropId, halPropConfig); // ② 转回 App 数据结构
CarPropertyEvent event = new CarPropertyEvent(
CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, carPropertyValue);
eventsToDispatch.add(event);
}
mPropertyHalListener.onPropertyChange(eventsToDispatch); // ③ 上抛给 CarPropertyService
}
这里又做了一次"翻译",方向和写/读时相反:HalPropValue → CarPropertyValue,并包装成 CarPropertyEvent(事件对象,区分"值变化"还是"出错")。
第 ③ 层 · CarPropertyService.onPropertyChange()
文件:service/src/com/android/car/CarPropertyService.java:776
public void onPropertyChange(List<CarPropertyEvent> events) {
Map<Client, List<CarPropertyEvent>> eventsToDispatch = new ArrayMap<>();
// ① 根据 mPropIdClientMap,算出"每个事件该发给哪些客户端"
for (CarPropertyEvent event : events) {
int propId = event.getCarPropertyValue().getPropertyId();
for (Client c : mPropIdClientMap.get(propId)) {
eventsToDispatch.get(c).add(event);
}
}
// ② 注意:在锁外面调用回调,避免死锁
for (Client client : eventsToDispatch.keySet()) {
client.onEvent(eventsToDispatch.get(client)); // Binder 回 App 进程!
}
}
这里是"扇出(fan-out)"的地方:一个事件,发给所有订阅了它的 App。client.onEvent() 这一步又跨进程回到了你的 App。
源码注释专门强调:一定要在锁外面调
onEvent,因为回调里可能反过来调 CarPropertyService 的方法,在锁内调用会死锁。这是个很经典的并发坑。
第 ② 层 · 回到 App 进程:CarPropertyEventListenerToService
文件:car-lib/src/android/car/hardware/property/CarPropertyManager.java:1232
private static class CarPropertyEventListenerToService
extends ICarPropertyEventListener.Stub {
@Override
public void onEvent(List<CarPropertyEvent> events) {
CarPropertyManager mgr = mCarPropertyManager.get();
if (mgr != null) {
mgr.handleEvents(events); // → mHandler.sendEvents(events)
}
}
}
事件回到 App 进程后:
handleEvents() → mHandler.sendEvents() → 最终交给前面那个
CarPropertyEventCallbackController,由它分发给你注册的每一个
CarPropertyEventCallback.onChangeEvent(CarPropertyValue value)。
到这里,你的 App 终于在回调里拿到了最新车速。 🎉
✅ 事件上报完整链路(这条最重要,建议背熟)
车身网络 (CAN) 值变化
└─ 厂商 VHAL 检测到变化
└─(Binder: IVehicleCallback)─ VehicleHal.onPropertyEvent() [VehicleHal.java:858]
└─ handleOnPropertyEvent → PropertyHalService.onHalEvents() [1284]
└─ HalPropValue→CarPropertyValue,包成 CarPropertyEvent
└─ CarPropertyService.onPropertyChange() [776] ←按属性扇出给多个客户端
└─(Binder: ICarPropertyEventListener)─ CarPropertyManager内部.onEvent() [1232]
└─ handleEvents → Handler → CarPropertyEventCallbackController
└─ 你的 CarPropertyEventCallback.onChangeEvent() ✅
6. 错误是怎么传回来的?(onPropertySetError)
set 是异步的,万一车端拒绝了(比如车速过高时不让开窗),错误怎么回来?
- VHAL 通过
IVehicleCallback.onPropertySetError上报 →VehicleHal.onPropertySetError()(VehicleHal.java:862) - →
PropertyHalService.onPropertySetError()(PropertyHalService.java:1371) - →
CarPropertyService.onPropertySetError()(CarPropertyService.java:821) 这里用之前记的mSetOperationClientMap找到当初是谁 set 的, - → 只把错误事件(
PROPERTY_EVENT_ERROR)回调给那一个 App。
这就是为什么 CarService 要费劲记录"谁设置的"——精准报错,不打扰无关的 App。
7. 异步读写
除了同步的 getProperty/setProperty,还有批量异步接口:
CarPropertyManager.getPropertiesAsync()/setPropertiesAsync()- →
ICarProperty.getPropertiesAsync()(AIDL,行号 = 8/10) - →
CarPropertyService→PropertyHalService.getCarPropertyValuesAsync()(行号 873/915 附近) - →
VehicleHal.getAsync()/setAsync()(VehicleHal.java:1394/1402) - →
VehicleStub.getAsync()/setAsync()
适合"一次读几十个属性"的场景,不阻塞线程,通过 IAsyncPropertyResultCallback 回结果。日常开发用同步接口就够了。
8. 关键类速查表
| 层 | 类 | 文件 | 职责一句话 |
|---|---|---|---|
| ② SDK | CarPropertyManager | car-lib/.../property/CarPropertyManager.java | App 的入口,get/set/registerCallback |
| ② SDK | CarPropertyEventCallbackController | car-lib/.../internal/CarPropertyEventCallbackController.java | App 进程内分发回调、按采样率过滤 |
| 桥 | ICarProperty.aidl | car-lib/.../property/ICarProperty.aidl | App↔CarService 的跨进程协议 |
| ③ Service | CarPropertyService | service/.../CarPropertyService.java | 权限校验、客户端管理、事件扇出 |
| ③ Service | PropertyHalService | service/.../hal/PropertyHalService.java | ID 转换、CarPropertyValue↔HalPropValue |
| ④ HAL胶水 | VehicleHal | service/.../hal/VehicleHal.java | 跟 VHAL 通信的总管、自动重试 |
| ④ HAL胶水 | VehicleStub/AidlVehicleStub | service/.../AidlVehicleStub.java | 屏蔽 AIDL/HIDL 差异 |
| 桥 | IVehicle.aidl | hardware/interfaces/automotive/vehicle/.../IVehicle.aidl | CarService↔VHAL 的跨进程协议 |
| ⑤ VHAL | 厂商 C++ 实现 | vendor 厂商代码 | 解析请求、收发 CAN/LIN 报文 |
9. 数据结构对照
| 用在哪 | 类名 | 长相 | 一句话 |
|---|---|---|---|
| App 层 | CarPropertyValue | propertyId + areaId + value(强类型) + status + timestamp | 给 Java 应用用的,好用 |
| App 层 | CarPropertyEvent | type(变化/出错) + CarPropertyValue | 事件信封,区分正常变化和错误 |
| App 层 | CarPropertyConfig | 属性的"说明书":类型、区域、采样率范围、读写权限 | getPropertyList 拿到的就是它 |
| HAL 层 | HalPropValue | int32[]/float[]/byte[] 等裸数组 | 贴近硬件、跨 HAL 高效传输 |
| HAL 层 | HalPropConfig | VHAL 侧的属性配置 | 和 CarPropertyConfig 互转 |
转换发生在 PropertyHalService:
- 下行
carPropertyValueToHalPropValueLocked()(PropertyHalService.java:1701) - 上行
HalPropValue.toCarPropertyValue()
10. 一句话总结全文
App 调
CarPropertyManager→ 跨进程到CarPropertyService(管权限和客户端)→PropertyHalService(管翻译)→VehicleHal/VehicleStub→ 再跨进程到厂商IVehicle实现 → CAN 总线 → 车。 读、写、订阅走的是同一条路,只是方向和同步/异步不同;事件上报则是这条路完整地倒着走一遍。
把这条链路在脑子里跑通,以后无论是"App 读不到信号"、"set 不生效"、"订阅没回调",你都能快速定位是哪一层断了:
- App 收不到回调 → 看
CarPropertyService.onPropertyChange有没有进来、mPropIdClientMap里有没有你。 - 值一直是默认值 → 看
PropertyHalService.onHalEvents是否被丢弃(unsupported property / payload check 失败)。 - set 报权限错 → 卡在
CarPropertyService的权限校验。 - 底层根本没动 → 看
VehicleHal/VehicleStub有没有把请求发到IVehicle。