夜雨聆风学习资料网

ARTICLE · 1123844

车载 Android 车控软件栈·第5篇 | car-lib · 属性读写:同步、异步与错误处理

车载 Android 车控软件栈·第5篇 | car-lib · 属性读写:同步、异步与错误处理

导读:同一行 getProperty(),targetSdk 33 时返回 null,升到 34 就抛异常——不少车机应用升级后的新崩溃都来自这里。本篇拆解 CarPropertyManager 的同步与异步读写:返回语义如何随 targetSdk 变化,异步写如何确认"车辆已执行",以及三套互不相同的错误码与异常如何对应。

本文基于 Android 16 QPR2(android-16.0.0_r4,VHAL V4)源码。文中 CarPropertyManager.java 指 packages/services/Car/car-lib/src/android/car/hardware/property/CarPropertyManager.java,API 可见性依据 car-lib/api/current.txt,行号均以该 tag 为准。


同一行读取属性的代码:

CarPropertyValue<Float> v = carPropertyManager.getProperty(
        Float.class, VehiclePropertyIds.HVAC_TEMPERATURE_SET, VehicleAreaSeat.SEAT_ROW_1_LEFT);

放在一个 targetSdkVersion 为 33 的应用里,车辆不支持这个属性时它返回 null;把 targetSdkVersion 升到 34,同样的情况下它改为抛出 IllegalArgumentException。空调控制器暂时离线时,在 33 的应用里它返回一个状态为"不可用"的值,在 34 的应用里则直接抛出 PropertyNotAvailableException。应用升级 targetSdk 之后在车机上出现的新崩溃,有不少就来自这里。

CarPropertyManager 的读写接口分为同步和异步两套,每套都有自己的返回语义、线程要求和错误表达方式,同步接口的行为还随 targetSdk 变化。本篇逐一拆解这些 API,最后整理出一张完整的错误码与异常对照表。


一、读写 API 全景

同步读

  • <E> CarPropertyValue<E> getProperty(Class<E>, int propertyId, int areaId):读取属性值,带类型检查
  • <E> CarPropertyValue<E> getProperty(int propertyId, int areaId):同上,不做类型检查
  • boolean getBooleanProperty(int, int)、float getFloatProperty(int, int)、int getIntProperty(int, int)、int[] getIntArrayProperty(int, int):直接返回值的便捷方法
  • boolean isPropertyAvailable(int, int):属性当前是否可用

同步写

  • <E> void setProperty(Class<E>, int propertyId, int areaId, E val):写入属性值
  • void setBooleanProperty(int, int, boolean)、void setFloatProperty(int, int, float)、void setIntProperty(int, int, int):便捷方法

异步读写

  • GetPropertyRequest generateGetPropertyRequest(int, int)、SetPropertyRequest<T> generateSetPropertyRequest(int, int, T):构造请求对象
  • void getPropertiesAsync(List<GetPropertyRequest>, [long timeoutInMs,] CancellationSignal, Executor, GetPropertyCallback):批量异步读
  • void setPropertiesAsync(List<SetPropertyRequest<?>>, [long timeoutInMs,] CancellationSignal, Executor, SetPropertyCallback):批量异步写

常量

  • ASYNC_GET_DEFAULT_TIMEOUT_MS = 10000:异步接口的默认超时(毫秒)
  • STATUS_ERROR_INTERNAL_ERROR = 1、STATUS_ERROR_NOT_AVAILABLE = 2、STATUS_ERROR_TIMEOUT = 3:异步失败的错误码
  • CAR_SET_PROPERTY_ERROR_CODE_*:写入失败时通过订阅回调报告的错误码,见第五节

所有接口都会跨进程调用 CarService,并最终等待 VHAL 的响应。同步接口的注释反复强调:"This method may take couple seconds to complete, so it needs to be called from a non-main thread."


二、同步读

2.1 getProperty

getProperty()(CarPropertyManager.java:2712、2835)的执行过程:

  1. 检查属性 ID 是否在车辆支持的列表中;
  2. 通过 ICarProperty.getProperty() 跨进程调用 CarService;
  3. 根据返回值的状态和 targetSdk,决定返回值、返回 null 还是抛出异常。

带 Class<E> 参数的版本会检查传入的类型与属性配置中的值类型是否一致,不一致时抛出 IllegalArgumentException。类型与属性 ID 的对应关系见上一篇:例如 FLOAT 类型的属性对应 Float.class,INT32_VEC 对应 Integer[].class。

2.2 行为随 targetSdk 变化

getProperty() 的行为在 Android 11(R)、12(S)、14(U)三个版本节点上发生过变化。按应用的 targetSdk 归纳如下(CarPropertyManager.java:2845-2902):

  • 车辆不支持该属性或区域
    • targetSdk < U:返回 null
    • targetSdk ≥ U:抛出 IllegalArgumentException
  • 值的状态为"不可用"
    • targetSdk < U:返回该值,由调用方检查状态
    • targetSdk ≥ U:抛出 PropertyNotAvailableException
  • 值的状态为"错误"
    • targetSdk < U:返回该值
    • targetSdk ≥ U:抛出 CarInternalErrorException
  • VHAL 返回"稍后重试"
    • targetSdk < R:返回 null
    • targetSdk ≥ R:抛出 PropertyNotAvailableAndRetryException
  • VHAL 返回其他错误
    • targetSdk < R:抛出 IllegalStateException
    • targetSdk ≥ R:按错误码抛出对应异常,见第五节

也就是说,只有 targetSdk 低于 34 的应用,才可能从同步 getProperty() 读到一个状态为"不可用"的值;34 及以上的应用,状态异常都以异常的形式出现。订阅得到的属性事件则不受这条规则影响,事件中的值始终可能处于不可用状态,仍需检查 getPropertyStatus()。

2.3 便捷方法

getBooleanProperty()、getFloatProperty()、getIntProperty()、getIntArrayProperty() 在 getProperty() 之上再做一层处理(handleNullAndPropertyStatus(),CarPropertyManager.java:2549-2572):

情况
targetSdk < S
targetSdk ≥ S
getProperty()
 返回 null
返回默认值
返回默认值
状态为"不可用"
返回默认值
抛出 PropertyNotAvailableException
状态为"错误"
返回默认值
抛出 CarInternalErrorException

默认值分别是 false、0f、0 和空数组。对 targetSdk 低于 31 的应用来说,返回 0 既可能是真实值,也可能表示读取失败,两者无法区分;需要区分时,应改用 getProperty() 并检查状态。

2.4 isPropertyAvailable

isPropertyAvailable(int propertyId, int areaId) 读取一次属性,只有状态为 STATUS_AVAILABLE 时返回 true(CarPropertyManager.java:2297 起)。属性不受支持、读取出错、VHAL 返回错误码时,它都返回 false 而不抛异常;缺少读权限时仍会抛出 SecurityException。它适合用来决定界面上某个控件是否可以操作。

2.5 并发限制与自动重试

同步调用在 CarService 中占用一个 binder 线程,直到 VHAL 返回结果。为了防止大量同步调用占满 binder 线程,CarPropertyService 限制同时进行的同步读写最多 16 个(SYNC_GET_SET_PROPERTY_OP_LIMIT,CarPropertyService.java:119、841),超出时返回一个内部的"稍后重试"错误码。

car-lib 收到这个错误码后会自动重试:每次等待 10 毫秒,最多尝试 10 次(runSyncOperation(),CarPropertyManager.java:2579-2601),仍然失败则按内部错误处理,抛出 CarInternalErrorException。这一机制对调用方透明,但它说明了两点:同步接口在高并发下会有额外延迟;需要批量读写多个属性时,应当使用一次异步调用,而不是在多个线程中并发发起同步调用。

同步读的返回语义随 targetSdk 变化:34 及以上的应用,不支持的属性和不可用的状态都以异常表示;更早的应用则可能得到 null、默认值或状态异常的值。


三、同步写

3.1 setProperty

setProperty(Class<E>, int propertyId, int areaId, E val)(CarPropertyManager.java:2977)以及 setBooleanProperty()、setFloatProperty()、setIntProperty() 三个便捷方法,把值封装为 CarPropertyValue 后调用 ICarProperty.setProperty()。CarService 在服务端检查写权限和参数,再交给 VHAL。

它的返回语义,在接口注释中说得很清楚(CarPropertyManager.java:2937-2952):

  • 方法返回不代表写入成功。 要确认结果,应当在写入之前订阅该 [propertyId, areaId]:收到值等于目标值的变化事件,表示写入成功;收到错误事件,表示写入失败。订阅必须在写入之前完成,否则回调可能在写入之后、订阅之前就已经发生。
  • 写入与当前值相同的值时,请求仍会发送到车辆,但不会产生新的变化事件。 如果希望避免无意义的写入,需要先读取当前值再决定是否写入。
  • 多个客户端同时写同一区域时,哪一次生效是未定义的,通常是最后到达车辆控制器的那一次(CarPropertyManager.java:224-225)。

3.2 异常

  • 车辆不支持该属性:抛出 IllegalArgumentException(所有版本)
  • 缺少写权限:抛出 SecurityException(所有版本)
  • VHAL 返回"稍后重试"
    • targetSdk < R:抛出 RuntimeException
    • targetSdk ≥ R:抛出 PropertyNotAvailableAndRetryException
  • VHAL 返回其他错误
    • targetSdk < R:抛出 IllegalStateException
    • targetSdk ≥ R:按错误码抛出对应异常,见第五节

写入的失败,还可能在方法返回之后才发生:请求已发到车辆,但车辆侧执行失败。这类失败通过订阅回调 onErrorEvent(propertyId, areaId, errorCode) 报告,错误码是 CAR_SET_PROPERTY_ERROR_CODE_*,见第五节。

同步写返回只代表请求已发到车辆总线;车辆是否执行,要在写入之前订阅,以变化事件或错误事件为准。


四、异步读写

4.1 请求与结果模型

异步接口以"请求对象"为单位工作,一次调用可以批量提交多个请求:

  • GetPropertyRequest:getRequestId()、getPropertyId()、getAreaId()。由 generateGetPropertyRequest() 创建
  • SetPropertyRequest<T>:同上,另有 getValue()、setWaitForPropertyUpdate(boolean)、setUpdateRateHz(float)。由 generateSetPropertyRequest() 创建
  • GetPropertyResult<T>:getRequestId()、getPropertyId()、getAreaId()、getValue()、getTimestampNanos()。读取成功的结果
  • SetPropertyResult:getRequestId()、getPropertyId()、getAreaId()、getUpdateTimestampNanos()。写入成功的结果
  • PropertyAsyncError:getRequestId()、getPropertyId()、getAreaId()、getErrorCode()、getDetailedErrorCode()。失败的结果
  • GetPropertyCallback / SetPropertyCallback:onSuccess(result)、onFailure(error)。结果回调

请求 ID 由 CarPropertyManager 内部的原子计数器生成,在同一个 CarPropertyManager 实例内唯一,用于把结果和请求对应起来。

4.2 getPropertiesAsync

getPropertiesAsync()(CarPropertyManager.java:3258、3315)的接口约定(CarPropertyManager.java:3226-3250):

  • 调用立即返回,结果稍后通过回调送达;
  • 每个请求恰好回调一次:成功调用 onSuccess,失败调用 onFailure。调用本身抛出异常时,任何回调都不会发生;
  • 某个请求对应的属性状态为"不可用"时,以 STATUS_ERROR_NOT_AVAILABLE 回调 onFailure;状态为"错误"时,以 STATUS_ERROR_INTERNAL_ERROR 回调;超时则为 STATUS_ERROR_TIMEOUT。因此,onSuccess 中得到的 GetPropertyResult,其值一定是可用的;
  • timeoutInMs 必须为正数,不传时默认 10 秒;
  • callbackExecutor 为 null 时,回调在默认的事件处理线程执行;回调中有耗时操作时,应当传入自己的 Executor;
  • 通过 CancellationSignal 取消后,保证不会再有任何回调;
  • 缺少某个属性的读权限时抛出 SecurityException,某个属性不受支持时抛出 IllegalArgumentException,整批请求都不会执行。
List<CarPropertyManager.GetPropertyRequest> requests = List.of(
        mgr.generateGetPropertyRequest(VehiclePropertyIds.HVAC_TEMPERATURE_SET,
                VehicleAreaSeat.SEAT_ROW_1_LEFT),
        mgr.generateGetPropertyRequest(VehiclePropertyIds.HVAC_TEMPERATURE_SET,
                VehicleAreaSeat.SEAT_ROW_1_RIGHT));

mgr.getPropertiesAsync(requests, /* cancellationSignal= */null, mExecutor,
new CarPropertyManager.GetPropertyCallback() {
@Override
publicvoidonSuccess(CarPropertyManager.GetPropertyResult<?> result){
// result.getValue() 一定可用;用 getAreaId() 区分主驾、副驾
            }

@Override
publicvoidonFailure(CarPropertyManager.PropertyAsyncError error){
// error.getErrorCode():NOT_AVAILABLE / INTERNAL_ERROR / TIMEOUT
            }
        });

4.3 setPropertiesAsync 与 waitForPropertyUpdate

异步写最重要的参数是 SetPropertyRequest.setWaitForPropertyUpdate(boolean),它决定"写入成功"的判定标准(CarPropertyManager.java:431-470):

  • true(默认):写入请求已送达车辆总线,并且属性值已经等于目标值(写入前就相等,或写入后收到了值更新为目标值的事件)
  • **false**:写入请求已送达车辆总线即可

默认值 true 解决的,正是同步 setProperty() 留给调用方的问题:不需要自己先订阅、再比对事件,onSuccess 本身就表示车辆状态已经变为目标值。在超时时间内没有等到值变为目标值,则以 STATUS_ERROR_TIMEOUT 回调 onFailure。

这个判定在 CarService 中实现:对需要等待的请求,PropertyHalService 会先发起一次"读取初始值"并订阅该属性的变化事件,然后再发送写入请求;写入已发送、且观察到值等于目标值时,才判定成功(PropertyHalService.java:322-329、2425-2451)。

以下情况必须把 waitForPropertyUpdate 设为 false:

  • 属性是只写的,无法读取,也就不会有更新事件;否则抛出 IllegalArgumentException;
  • 属性表示一个动作而不是状态,例如按键;
  • HVAC_TEMPERATURE_VALUE_SUGGESTION,它回报的值与写入的值本来就不同;否则抛出 IllegalArgumentException。

setUpdateRateHz(float) 只对连续变化的属性、且 waitForPropertyUpdate 为 true 时有意义,用于设置等待期间订阅更新事件的频率。

即使等到了目标值,接口注释也说明:这只保证在请求发出到回调之间的某个时刻,属性值曾等于目标值;如果其他客户端同时在修改同一属性,回调执行时的值可能已经又变了。

以"主驾调到 22℃"为例,默认设置下的时序如下:

4.4 异步写的示例

CarPropertyManager.SetPropertyRequest<Float> request = mgr.generateSetPropertyRequest(
        VehiclePropertyIds.HVAC_TEMPERATURE_SET, VehicleAreaSeat.SEAT_ROW_1_LEFT, 22.0f);
// 默认 waitForPropertyUpdate = true:onSuccess 表示车辆状态已经变为 22.0

CancellationSignal cancel = new CancellationSignal();
mgr.setPropertiesAsync(List.of(request), /* timeoutInMs= */5000, cancel, mExecutor,
new CarPropertyManager.SetPropertyCallback() {
@Override
publicvoidonSuccess(CarPropertyManager.SetPropertyResult result){
// 车辆已执行;result.getUpdateTimestampNanos() 为值更新的时间
            }

@Override
publicvoidonFailure(CarPropertyManager.PropertyAsyncError error){
// TIMEOUT:规定时间内未观察到值变为 22.0
// NOT_AVAILABLE:属性暂不可用,可结合 getDetailedErrorCode() 给出具体提示
            }
        });
// 界面退出时:cancel.cancel(),之后不会再收到回调

异步写默认等到属性值真正变为目标值才回调成功,是确认"车辆已执行"最直接的方式;只写属性和动作类属性须关闭这一等待。


五、错误码与异常体系

5.1 VHAL 的状态码

所有错误最初都来自 VHAL 返回的一个整数状态码:低 16 位是系统定义的错误码,高 16 位留给厂商自定义(CarPropertyErrorCodes.java:116-117)。系统错误码的取值(VehicleHalStatusCode.java:36-98):

  • STATUS_OK(0):成功
  • STATUS_TRY_AGAIN(1):暂时失败,稍后重试可能成功
  • STATUS_INVALID_ARG(2):参数无效
  • STATUS_NOT_AVAILABLE(3):属性不可用
  • STATUS_ACCESS_DENIED(4):车辆拒绝访问
  • STATUS_INTERNAL_ERROR(5):内部错误
  • STATUS_NOT_AVAILABLE_DISABLED … STATUS_NOT_AVAILABLE_SUBSYSTEM_NOT_CONNECTED(6–11):不可用的细分原因:功能关闭、车速过低、车速过高、能见度差、安全原因、子系统未连接

5.2 同步接口:映射为异常

同步接口(targetSdk ≥ R)把状态码映射为具体异常,映射集中在 CarPropertyErrorCodes.checkAndMaybeThrowException()(CarPropertyErrorCodes.java:297-322):

  • NOT_AVAILABLE 及其细分(3、6–11) → PropertyNotAvailableException(继承自 IllegalStateException),附带 getDetailedErrorCode(),取值见 PropertyNotAvailableErrorCode
  • TRY_AGAIN → PropertyNotAvailableAndRetryException(继承自 IllegalStateException)
  • ACCESS_DENIED → PropertyAccessDeniedSecurityException(继承自 SecurityException)
  • INTERNAL_ERROR 及其他 → CarInternalErrorException(继承自 RuntimeException)

此外,调用方自身缺少权限时抛出的是普通的 SecurityException,由 CarService 在检查权限时抛出;而 PropertyAccessDeniedSecurityException 表示的是车辆拒绝了访问,两者需要区分。

PropertyNotAvailableErrorCode 的取值为:NOT_AVAILABLE = 0、NOT_AVAILABLE_DISABLED = 1、NOT_AVAILABLE_SPEED_LOW = 2、NOT_AVAILABLE_SPEED_HIGH = 3、NOT_AVAILABLE_POOR_VISIBILITY = 4、NOT_AVAILABLE_SAFETY = 5。r4 中新增的"子系统未连接"暂时映射为 NOT_AVAILABLE,源码中留有 TODO(CarPropertyErrorCodes.java:100-104)。

据此,同步调用的异常处理可以这样组织:

try {
float t = mgr.getFloatProperty(VehiclePropertyIds.HVAC_TEMPERATURE_SET,
            VehicleAreaSeat.SEAT_ROW_1_LEFT);
    showTemperature(t);
} catch (PropertyNotAvailableAndRetryException e) {
// 暂时失败:稍后重试
} catch (PropertyNotAvailableException e) {
// 不可用:按 e.getDetailedErrorCode() 提示原因,例如车速过高
} catch (PropertyAccessDeniedSecurityException e) {
// 车辆拒绝访问
} catch (SecurityException e) {
// 应用缺少权限
} catch (CarInternalErrorException e) {
// 车辆内部错误
} catch (IllegalArgumentException e) {
// 车辆不支持该属性或区域(targetSdk ≥ U)
}

PropertyNotAvailableAndRetryException 与 PropertyNotAvailableException 都继承自 IllegalStateException,PropertyAccessDeniedSecurityException 继承自 SecurityException,因此 catch 的顺序必须子类在前。

5.3 异步接口:错误码

异步接口不抛异常,失败通过 PropertyAsyncError 回调:

  • getErrorCode():STATUS_ERROR_INTERNAL_ERROR(1)、STATUS_ERROR_NOT_AVAILABLE(2)、STATUS_ERROR_TIMEOUT(3)
  • getDetailedErrorCode()(flag car_property_detailed_error_codes):DetailedErrorCode:NO_DETAILED_ERROR_CODE = 0,以及与不可用细分原因对应的 1–5

5.4 订阅回调中的写入错误

同步 setProperty() 在返回之后才发生的失败,通过订阅回调 CarPropertyEventCallback.onErrorEvent() 报告。onErrorEvent 的签名与触发条件在订阅篇中说明,这里只列错误码 CAR_SET_PROPERTY_ERROR_CODE_*:

常量
值
CAR_SET_PROPERTY_ERROR_CODE_TRY_AGAIN
1
CAR_SET_PROPERTY_ERROR_CODE_INVALID_ARG
2
CAR_SET_PROPERTY_ERROR_CODE_PROPERTY_NOT_AVAILABLE
3
CAR_SET_PROPERTY_ERROR_CODE_ACCESS_DENIED
4
CAR_SET_PROPERTY_ERROR_CODE_UNKNOWN
5

同一个 VHAL 状态码,在同步接口中表现为异常,在异步接口中表现为 PropertyAsyncError,在写入之后的回调中表现为 onErrorEvent 的错误码;三种形式的数值互不相同,处理时要以所用接口的常量为准。


六、同步与异步的选择

  • 界面初始化,一次读取多个属性:getPropertiesAsync()。一次调用批量完成,不占用调用线程,不会触发同步并发限制
  • 用户操作触发的写入,需要确认车辆已执行:setPropertiesAsync(),waitForPropertyUpdate = true。回调即代表执行结果,不需要自行订阅比对
  • 按键、只写属性等动作类写入:setPropertiesAsync(),waitForPropertyUpdate = false。这类属性没有可等待的状态
  • 后台线程中的偶发读取:同步 getProperty()。代码简单,但须在非主线程调用
  • 持续关注属性变化:订阅,而不是轮询读取。订阅在下一篇展开

结语:读写 API 检查清单

  • 线程:同步读写不在主线程调用;异步回调中的耗时操作交给自己的 Executor
  • targetSdk:升级到 34 前,检查所有 getProperty() 的调用点:返回 null 的分支会变为 IllegalArgumentException,状态不可用会变为异常
  • 便捷方法:targetSdk 低于 31 时,返回的默认值无法与真实值区分,需要区分时改用 getProperty()
  • 写入结果:同步写先订阅再写入,以事件为准;异步写默认等待值更新
  • 批量操作:用一次异步调用代替多个线程并发的同步调用
  • 异常处理:区分应用缺权限的 SecurityException 与车辆拒绝的 PropertyAccessDeniedSecurityException;catch 时子类在前
  • 不可用原因:使用 getDetailedErrorCode() 给用户具体提示
  • 取消:界面销毁时通过 CancellationSignal 取消未完成的异步请求

读写解决的是"某一时刻的值"。对于车速、档位、空调状态这类持续变化的信号,应用需要的是订阅。后续篇目将展开 CarPropertyManager 的订阅接口。


参考资料

  • CarPropertyManager(developer.android.com):读写接口、异步请求与错误码的公开 API 说明
  • VHAL interface:VHAL 的异步 getValues / setValues 与状态码
  • AOSP 源码(android-16.0.0_r4):CarPropertyManager.java、CarPropertyErrorCodes.java、CarPropertyService.java、PropertyHalService.java

如果觉得有帮助,欢迎点赞、在看、转发三连。


系列导航:《车载 Android 车控软件栈》从 CarService 全景出发,沿应用 → car-lib → CarService → VHAL 这条链路逐层拆解 Android Automotive 的车控框架;另一个系列《车载 Camera 软件栈》讲应用层如何获取摄像头画面。公众号点「合集」看全部。

相关学习资料