ARTICLE · 1139352
车载 Android 车控软件栈·第6篇 | Car-lib · 属性订阅:采样率、VUR 与回调线程
导读:车停在路边一分钟,用旧接口订阅车速收到 600 个全是 0 的事件,换成新接口只收到 1 个。差别来自"值变化才上报"(VUR)。本篇拆解
CarPropertyManager的订阅 API:采样率如何夹取、分辨率如何取整、初始值何时到达、回调在哪个线程,以及从registerCallback()迁移时要注意什么。
本文基于 Android 16 QPR2(android-16.0.0_r4,VHAL V4)源码。文中
CarPropertyManager.java、Subscription.java位于packages/services/Car/car-lib/src/android/car/hardware/property/,car-lib 内部类位于car-lib/src/com/android/car/internal/property/;API 可见性依据car-lib/api/current.txt,行号均以该 tag 为准。
一个Android应用用旧接口订阅车速,采样率 10Hz:
carPropertyManager.registerCallback(callback, VehiclePropertyIds.PERF_VEHICLE_SPEED, 10f);
车停在路边一分钟,车速始终是 0,应用却收到了 600 个事件,每个事件的值都是 0。把同样的订阅换成新接口 subscribePropertyEvents(),在车辆支持该特性的前提下,同样一分钟只收到 1 个事件。
两个接口的差别,在于新接口默认开启了"值变化才上报"(Variable Update Rate,以下简称 VUR)。这只是订阅 API 众多细节中的一个:采样率如何被夹取、分辨率如何取整、回调在哪个线程执行、订阅之后第一个事件何时到达、多个订阅如何合并。本篇逐一拆解订阅相关的 API,以及它们在 car-lib 中的实现。
一、订阅 API 全景
订阅
subscribePropertyEvents(int propertyId, CarPropertyEventCallback)(公开,flagvariable_update_rate)subscribePropertyEvents(int propertyId, float updateRateHz, CarPropertyEventCallback)(同上)subscribePropertyEvents(int propertyId, int areaId, CarPropertyEventCallback)(同上)subscribePropertyEvents(int propertyId, int areaId, float updateRateHz, CarPropertyEventCallback)(同上)subscribePropertyEvents(List<Subscription>, Executor, CarPropertyEventCallback)(公开,flagbatched_subscriptions)
取消订阅
unsubscribePropertyEvents(CarPropertyEventCallback)(公开,flagbatched_subscriptions)unsubscribePropertyEvents(int propertyId, CarPropertyEventCallback)(同上)
订阅选项
Subscription、Subscription.Builder(公开,flagbatched_subscriptions)
回调
CarPropertyEventCallback:onChangeEvent()、onErrorEvent()(公开)
采样率常量
SENSOR_RATE_ONCHANGE = 0、SENSOR_RATE_NORMAL = 1、SENSOR_RATE_UI = 5、SENSOR_RATE_FAST = 10、SENSOR_RATE_FASTEST = 100(单位 Hz)(公开)
已废弃
registerCallback(CarPropertyEventCallback, int propertyId, float updateRateHz)、unregisterCallback(CarPropertyEventCallback[, int propertyId])(公开,已废弃)
前四个 subscribePropertyEvents 重载都是便捷写法,内部都会构造 Subscription,最终调用第五个批量重载(CarPropertyManager.java:1265、1295、1327、1391、1508)。
二、subscribePropertyEvents
2.1 五个重载
(propertyId, callback):订阅全部区域,采样率 1Hz,回调在默认线程(propertyId, updateRateHz, callback):订阅全部区域,采样率由参数指定,回调在默认线程(propertyId, areaId, callback):订阅指定区域,采样率 1Hz,回调在默认线程(propertyId, areaId, updateRateHz, callback):订阅指定区域,采样率由参数指定,回调在默认线程(List<Subscription>, executor, callback):区域与采样率由每个Subscription指定(未指定区域时为全部区域),回调在传入的 Executor 上,传 null 时在默认线程
"默认线程"是创建 Car 时传入的 Handler 所在线程,未传 Handler 时为主线程,见第五节。
2.2 接口约定
批量重载的注释(CarPropertyManager.java:1398-1506)规定了以下规则:
采样率
对 STATIC和ONCHANGE属性,采样率不起作用;对 CONTINUOUS属性,采样率表示车辆轮询该属性的频率;采样率高于属性的 getMaxSampleRate()时按最大值注册,低于getMinSampleRate()时按最小值注册,不会抛出异常;通过 Subscription.Builder.setUpdateRateHz()设置时,取值范围必须在 0 到 100 之间,超出时抛出IllegalArgumentException。
订阅关系
同一个 [propertyId, areaId]可以注册多个回调,所有回调都会被调用;对于同一个 [propertyId, areaId, callback]组合,只保留一份采样率和 VUR 设置,重复订阅会覆盖原有设置;同一次调用中,同一属性可以出现在多个 Subscription里,但它们的区域不能重叠,否则抛出IllegalArgumentException;一个回调只能绑定一个 Executor:想换 Executor,必须先取消订阅。
异常
缺少某个属性的读权限: SecurityException;区域重叠、Executor 冲突、属性不受支持: IllegalArgumentException。
订阅适合的场景是"持续关注一个值",而读取适合"只关心某一时刻的值"。 对于状态随时变化的信号,用订阅代替轮询读取,既能及时获知变化,也不占用 CarService 的同步调用配额。
三、Subscription:订阅选项
3.1 API
Subscription.Builder
Builder(int propertyId):创建某个属性的订阅选项addAreaId(int areaId):添加区域;一个都不添加时订阅全部区域setUpdateRateHz(float):采样率,0 到 100 HzsetUpdateRateUi()/setUpdateRateNormal()/setUpdateRateFast()/setUpdateRateFastest():分别等于 5、1、10、100 HzsetVariableUpdateRateEnabled(boolean):是否开启 VUR,flagvariable_update_ratesetResolution(float):分辨率,flagsubscription_with_resolutionbuild():构造Subscription;同一个 Builder 只能调用一次,否则抛出IllegalStateException
Subscription
getPropertyId()、getAreaIds()、getUpdateRateHz()、isVariableUpdateRateEnabled()、getResolution():读取已设置的选项
3.2 VUR:值变化才上报
VUR 只对 CONTINUOUS 属性有意义,它决定了在固定采样率下,值没有变化时是否还要上报(Subscription.java:240-270):
开启( subscribePropertyEvents的默认值):只在值或状态变化时收到事件,行为与ONCHANGE属性相同关闭:按采样率周期性收到事件,即使值没有变化
源码注释举了一个例子:以 10Hz 订阅车速,车辆在第 0 秒静止、第 1 秒开始以速度 1 行驶。开启 VUR 时,应用在第 0 秒收到速度 0、在第 1 秒收到速度 1,共 2 个事件;关闭 VUR 时,0 到 1 秒收到 10 个速度 0 的事件,1 到 2 秒收到 10 个速度 1 的事件。
VUR 能否开启,还取决于车辆是否支持:某个区域的 AreaIdConfig.isVariableUpdateRateSupported() 返回 false 时,即使订阅方要求开启,该区域的 VUR 也始终关闭。car-lib 在订阅时按区域检查这一点,必要时会把一个订阅拆成"支持 VUR 的区域"和"不支持 VUR 的区域"两部分分别处理(InputSanitizationUtils.sanitizeEnableVariableUpdateRate())。
源码注释对 VUR 的建议是:"STRONGLY RECOMMENDED NOT TO DISABLE variable update rate",除非应用依赖持续到达的事件,例如用于系统健康检查。
3.3 resolution:分辨率
分辨率同样只对 CONTINUOUS 属性有意义,它决定上报的值取整到什么精度(Subscription.java:285-320):
取值必须是 10 的整数次幂,例如 0.01、0.1、1.0、10.0,否则 setResolution()直接抛出IllegalArgumentException;默认值为 0,表示不取整,保留最高精度; 与 VUR 配合使用效果最好:取整之后的值没有变化,就不会上报。
源码注释的例子是:以分辨率 1.0 订阅车速,速度从 24.2 变为 24.3 时,取整后都是 24,不会上报;从 24.4 变为 24.6 时,取整后从 24 变为 25,才会上报。
VUR 和分辨率共同决定了"多小的变化值得上报":应用应当只订阅自己需要的最低精度,并保持 VUR 开启,以减少不必要的事件。
3.4 批量订阅示例
// 车速:10Hz,分辨率 1 km/h,保持 VUR 开启
Subscription speed = new Subscription.Builder(VehiclePropertyIds.PERF_VEHICLE_SPEED)
.setUpdateRateHz(10f)
.setResolution(1.0f)
.build();
// 主驾与副驾的设定温度(ONCHANGE 属性,采样率不起作用)
Subscription temp = new Subscription.Builder(VehiclePropertyIds.HVAC_TEMPERATURE_SET)
.addAreaId(VehicleAreaSeat.SEAT_ROW_1_LEFT)
.addAreaId(VehicleAreaSeat.SEAT_ROW_1_RIGHT)
.build();
carPropertyManager.subscribePropertyEvents(List.of(speed, temp), mExecutor, mCallback);
四、CarPropertyEventCallback 与初始值事件
4.1 回调方法
void onChangeEvent(CarPropertyValue value):属性值或状态变化时;VUR 关闭的连续属性按采样率周期调用;每次新订阅时,调用一次以送达初始值void onErrorEvent(int propertyId, int areaId):最近一次同步setProperty()在车辆侧失败时(必须实现)default void onErrorEvent(int propertyId, int areaId, int errorCode):同上,带错误码CAR_SET_PROPERTY_ERROR_CODE_*;默认实现会调用两参版本
接口注释建议:新代码实现三参版本,把两参版本实现为空方法(CarPropertyManager.java:200-235)。需要注意,**onErrorEvent 只报告同步 setProperty() 的失败**,异步 setPropertiesAsync() 的失败通过其自身的 onFailure() 报告,不会触发这里的回调。多个客户端同时写同一区域时,错误只会发给最近写入的客户端。
4.2 初始值事件
订阅时,接口注释约定了初始值的行为:
订阅成功后,回调会立即收到每个 [propertyId, areaId]的当前值;当前值不可读或处于错误状态时,收到的是状态为"不可用"或"错误"的事件; 因电源状态关闭而不可读的属性,在电源开启后会收到一次包含初始值的事件。
因此,应用不需要在订阅之后再额外读取一次当前值,订阅本身就会送达初始值。
有一个边界情况:同一个回调对一个已经订阅的属性再次订阅,而订阅参数没有变化时,car-lib 在客户端合并之后可能不会向 CarService 发送任何请求,也就不会产生新的初始值事件。r4 为此增加了补偿逻辑:aconfig flag always_send_initial_value_event 开启、且应用的 targetSdk 不低于 36 时,car-lib 会对这些没有下发的 [propertyId, areaId] 主动请求 CarService 补发一次初始值(CarPropertyManager.java:1633-1666)。
4.3 状态变化与事件的关系
接口注释还提醒了一点:属性状态变化时,不保证一定产生变化事件,调用方不应依赖变化事件来判断属性是否可用。对于依赖某个电源状态的属性,应当同时订阅对应的电源属性,例如空调相关属性依赖 HVAC_POWER_ON,由后者判断前者是否可用。
每个收到的事件,仍需先检查 getPropertyStatus(),状态为 STATUS_AVAILABLE 时才使用 getValue()。
订阅会立即送达初始值,之后在值或状态变化时回调;事件中的值随时可能不可用,使用前必须检查状态。
五、回调线程
属性事件从 CarService 到达应用后,要经过三跳才进入 onChangeEvent()(CarPropertyManager.java:1692-1775、3822-3860):

binder 线程:CarService 以 oneway 调用把一批事件发给应用进程中的 ICarPropertyEventListener;分组与过滤:car-lib 按属性 ID 分组,再按每个回调自己的订阅参数过滤。这一步在哪个线程执行,取决于 aconfig flag handle_property_events_in_binder_thread:关闭时切换到事件处理线程执行,开启时直接在 binder 线程执行,省去一次线程切换;Executor:最终通过 executor.execute()调用回调。订阅时传入了 Executor 就用它;传入 null 或使用四个便捷重载时,使用CarPropertyManager的默认 Executor,即创建Car时传入的 Handler 所在线程,未传 Handler 时为主线程(CarPropertyManager.java:1081-1088)。
对应用而言,需要记住的只有最后一跳:回调运行在订阅时传入的 Executor 上;不传时运行在 Car 的 Handler 线程,默认是主线程。 车速这类高频属性,回调中如果有较重的处理,应传入自己的 Executor,避免阻塞主线程。
六、订阅在 car-lib 中的处理
6.1 订阅请求的处理
一次 subscribePropertyEvents() 调用,在 car-lib 中依次经过以下步骤(subscribePropertyEventsInternal(),CarPropertyManager.java:1537-1667):
检查同一次调用中的区域是否重叠; 检查读权限,缺少时抛出 SecurityException;参数整理:未指定区域的补全为全部区域;采样率按属性的最小、最大采样率夹取;检查分辨率;按区域检查 VUR 是否支持,必要时拆分订阅; 检查回调的 Executor 是否与已绑定的一致; 计算差异:car-lib 内部的 SubscriptionManager以"暂存—提交"两阶段管理本进程内的全部订阅,先把新订阅暂存,与当前生效的订阅比较,只把变化的部分通过ICarProperty.registerListener()/unregisterListener()发给 CarService;为回调建立本地的事件过滤器。
第 5 步意味着:同一个进程内的多个回调订阅同一属性时,car-lib 先在本地合并,CarService 看到的是这个进程合并之后的订阅;合并规则与服务端相同,采样率取最大值、分辨率取最细、VUR 只有全部开启时才开启。CarService 再在所有进程之间合并一次,VHAL 看到的是 CarService 合并后的一份订阅,再与其他 native 客户端的订阅一起合并。服务端的合并在 CarService 篇中展开。
6.2 客户端的二次过滤
合并之后,下发给 VHAL 的是"所有订阅方中要求最高"的参数。例如回调 A 要求 1Hz、回调 B 要求 10Hz,VHAL 按 10Hz 上报,事件到达应用进程后,car-lib 还要为回调 A 过滤掉多余的事件。这一步由每个回调各自的事件追踪器完成(ContCarPropertyEventTracker.java):
按采样率过滤:两个事件之间的间隔小于回调要求的周期时,丢弃后一个。计算周期时乘以 0.95 的系数( UPDATE_PERIOD_OFFSET,ContCarPropertyEventTracker.java:38),留出 5% 的余量,避免时间戳的抖动导致本该送达的事件被误丢;按 VUR 过滤:回调开启了 VUR,而新事件的值和状态都与上一个相同时,丢弃; 按分辨率取整:先按回调要求的分辨率取整,再判断值是否变化。
因此,每个回调收到的事件,都符合它自己订阅时的参数,不会因为同进程内其他回调要求更高而多收事件。
七、取消订阅
unsubscribePropertyEvents(CarPropertyEventCallback):取消该回调的全部订阅unsubscribePropertyEvents(int propertyId, CarPropertyEventCallback):取消该回调对某个属性的订阅(全部区域)
取消订阅没有按区域取消的接口。需要只保留部分区域时,先取消该属性的订阅,再按需要的区域重新订阅。
接口注释还提醒:如果取消订阅的同时有事件正在分发,方法返回之后仍可能收到事件,应用应当忽略取消订阅之后收到的所有事件(CarPropertyManager.java:1883-1893)。另外,第 3 篇讲过,CarService 重启后所有 Manager 都会失效,订阅也随之失效,需要在 ready=true 时重新获取 CarPropertyManager 并重新订阅。
八、从 registerCallback 迁移
旧接口 registerCallback() 已经废弃,在 r4 中它本身也已改为调用新的订阅框架(CarPropertyManager.java:1197-1241)。两者的差异如下:
VUR 默认值 registerCallback():关闭,连续属性按采样率周期上报subscribePropertyEvents():开启,只在值变化时上报采样率超出 0 到 100 registerCallback():自动夹取到范围内subscribePropertyEvents():通过Subscription.Builder设置时抛出IllegalArgumentException回调线程 registerCallback():默认线程subscribePropertyEvents():可以指定 Executor批量订阅 registerCallback():每次一个属性subscribePropertyEvents():一次可订阅多个属性、多个区域分辨率 registerCallback():不支持subscribePropertyEvents():支持取消订阅 registerCallback():unregisterCallback()subscribePropertyEvents():unsubscribePropertyEvents()
迁移时,最需要注意的是 VUR 默认值的变化。registerCallback() 的废弃说明写道:如果希望保持"值不变也收到事件"的旧行为,需要显式调用 setVariableUpdateRateEnabled(false),但这会降低系统性能(CarPropertyManager.java:1097-1103)。依赖周期性事件做心跳检测的代码,迁移后要么显式关闭 VUR,要么改为其他检测方式。
// 迁移前
carPropertyManager.registerCallback(callback, VehiclePropertyIds.PERF_VEHICLE_SPEED,
CarPropertyManager.SENSOR_RATE_FAST);
// 迁移后:默认开启 VUR,只在车速变化时回调
carPropertyManager.subscribePropertyEvents(VehiclePropertyIds.PERF_VEHICLE_SPEED,
CarPropertyManager.SENSOR_RATE_FAST, callback);
// 迁移后,且需要保留周期性事件(例如用于健康检查)
carPropertyManager.subscribePropertyEvents(List.of(
new Subscription.Builder(VehiclePropertyIds.PERF_VEHICLE_SPEED)
.setUpdateRateFast()
.setVariableUpdateRateEnabled(false)
.build()), mExecutor, callback);
另有一处文档与源码不一致,迁移时可能会遇到:几个便捷重载的 @see 引用了一个带 boolean 参数的五参数重载 subscribePropertyEvents(int, int, float, boolean, CarPropertyEventCallback)(CarPropertyManager.java:1261、1291、1323),但 r4 源码中并没有这个方法,需要控制 VUR 时应使用 Subscription.Builder。
结语:订阅 API 检查清单
选择接口:新代码使用 subscribePropertyEvents();多个属性用List<Subscription>一次订阅采样率:只对连续属性有效,超出属性的采样率范围会被夹取; ONCHANGE属性不用设置VUR:保持默认开启;只有依赖周期事件时才关闭,并确认区域支持 VUR 分辨率:设置为业务需要的最低精度,必须是 10 的整数次幂 初始值:订阅会立即送达初始值,不需要另外读取 状态:每个事件先检查 getPropertyStatus();可用性依赖电源的属性,同时订阅对应的电源属性线程:高频属性传入自己的 Executor;一个回调只绑定一个 Executor 写入错误: onErrorEvent只报告同步setProperty()的失败,实现三参版本取消订阅:取消之后收到的事件一律忽略;CarService 重启后重新订阅 迁移:从 registerCallback()迁移时,注意 VUR 默认值从关闭变为开启
至此,car-lib 中与车属性相关的连接、数据模型、读写和订阅接口都已讲完。后续篇目将转到 CarService 一侧,看这些请求到达服务端之后如何被检查、合并和分发。
参考资料
CarPropertyManager(developer.android.com)、Subscription、Subscription.Builder:订阅接口的公开 API 说明 VHAL interface:VHAL 的订阅选项(采样率、分辨率、VUR) AOSP 源码( android-16.0.0_r4):CarPropertyManager.java、Subscription.java、car-lib/src/com/android/car/internal/property
如果觉得有帮助,欢迎点赞、在看、转发三连。
系列导航:《车载 Android 车控软件栈》从 CarService 全景出发,沿应用 → car-lib → CarService → VHAL 这条链路逐层拆解 Android Automotive 的车控框架;另一个系列《车载 Camera 软件栈》讲应用层如何获取摄像头画面。公众号点「合集」看全部。