4. 接口与回调:定义服务端接口、客户端回调接口、oneway与同步调用
这一章,我们来聊聊 HIDL 接口设计里最核心的交互模式。说白了,就是服务端和客户端怎么“说话”,以及说话的方式是“你等我回”还是“说完就跑”。
我在做第一个 HIDL 服务时,就踩过 oneway 的坑。当时以为 all 接口都设成 oneway 能提高性能,结果客户端拿不到返回值,整个逻辑全乱套了。嗯,从那以后我才认真琢磨这几种调用方式的区别。
4.1 服务端接口 vs 客户端回调接口
先看一个最基础的例子。服务端接口,就是服务端暴露给客户端调用的方法。客户端回调接口,则是客户端实现、服务端反过来调用的方法。两者合在一起,才能构成完整的双向通信。
我习惯把服务端接口叫做 “主接口”,把回调接口叫做 “反向接口”。你想想看,客户端调服务端是正向,服务端调客户端就是反向。
核心概念:服务端接口由服务端实现,客户端调用;回调接口由客户端实现,服务端调用。
在 HIDL 中,定义回调接口的方式很简单——在同一个 .hal 文件里再声明一个 interface 就行。比如:
// ISensorManager.hal
package vendor.example.sensor@1.0;
interface ISensorManager {
// 服务端接口:客户端调用来注册监听
registerCallback(ISensorCallback callback) generates (bool result);
// 服务端接口:客户端调用来获取传感器列表
getSensorList() generates (vec<SensorInfo> list);
};
// 回调接口:服务端调用来通知客户端
interface ISensorCallback {
oneway onSensorEvent(SensorEvent event);
oneway onError(int32_t errorCode);
};
注意看,回调接口里的方法我用了 oneway 关键字。为什么?因为服务端回调客户端时,不希望被阻塞。如果回调是同步的,服务端线程就得等客户端处理完才能继续干活,这在实时性要求高的场景下会出大问题。
4.2 oneway 调用:说完就跑
oneway 调用,说白了就是“发完即忘”。调用方发出请求后,不等待返回值,直接返回。被调用方在另一个线程里异步处理。
| 特性 | 同步调用 | oneway 调用 |
|---|---|---|
| 返回值 | 有,调用方等待 | 无,调用方不等待 |
| 阻塞行为 | 阻塞调用线程 | 不阻塞,立即返回 |
| 适用场景 | 需要结果,如查询配置 | 通知、事件上报、日志 |
| 线程模型 | 调用方线程直接执行 | 由 Binder 线程池处理 |
| 可靠性 | 高,调用方知道结果 | 低,可能丢失或乱序 |
我曾经在一个音频 HAL 服务里,把事件上报接口设成了同步调用。结果每次上报音频焦点变化时,服务端线程都被卡住,导致其他客户端无法及时获取音频参数。改成 oneway 后,问题立刻解决。
避坑指南:oneway 调用不保证顺序和送达。如果业务逻辑依赖严格顺序,比如“先打开设备再配置参数”,就不能用 oneway。我曾经在 camera HAL 里犯过这个错,预览画面一直黑屏,查了半天才发现是 open 和 configure 的 oneway 调用乱序了。
4.3 同步调用:你等我回
同步调用是最常见的模式。客户端调用服务端接口后,线程挂起,直到服务端返回结果才继续执行。HIDL 中默认就是同步调用,除非你显式加上 oneway。
同步调用的好处是简单、可靠。你调一个 getSensorList(),直接拿到结果,不用操心异步回调。坏处是如果服务端处理慢,客户端线程就会一直等。
我建议这样选型:
- 查询类操作(get、read、query)→ 同步调用
- 命令类操作(set、write、configure)→ 同步调用,但注意超时
- 事件通知类(onEvent、onError、onData)→ oneway 调用
- 高频数据流(帧数据、传感器流)→ oneway 调用
4.4 完整示例:温度监控服务
我们来看一个完整的例子。假设要做一个温度监控服务,客户端注册回调,服务端定期上报温度变化。
先定义接口:
// ITemperatureMonitor.hal
package vendor.example.temp@1.0;
interface ITemperatureMonitor {
// 同步:客户端注册回调,返回是否成功
registerCallback(ITemperatureCallback callback) generates (bool success);
// 同步:客户端获取当前温度
getCurrentTemperature() generates (float tempCelsius);
// 同步:客户端设置上报间隔(毫秒)
setReportInterval(int32_t intervalMs) generates (bool success);
};
interface ITemperatureCallback {
// oneway:服务端上报温度变化
oneway onTemperatureChanged(float tempCelsius, int64_t timestampMs);
// oneway:服务端上报告警
oneway onAlert(int32_t alertCode, string message);
};
服务端实现的关键逻辑:
// TemperatureMonitor.cpp 片段
Return<bool> TemperatureMonitor::registerCallback(
const sp<ITemperatureCallback>& callback) {
// 保存回调对象
mCallback = callback;
ALOGI("Callback registered");
return true;
}
// 在另一个线程中定时上报
void TemperatureMonitor::reportLoop() {
while (mRunning) {
float temp = readSensor();
if (mCallback != nullptr) {
// oneway 调用,不阻塞
mCallback->onTemperatureChanged(temp, getTimestamp());
}
usleep(mIntervalMs * 1000);
}
}
客户端实现回调:
// Client.cpp 片段
class TemperatureCallback : public ITemperatureCallback {
public:
Return<void> onTemperatureChanged(float temp, int64_t ts) override {
ALOGI("Temperature: %.1f°C at %lld", temp, (long long)ts);
return Void();
}
Return<void> onAlert(int32_t code, const hidl_string& msg) override {
ALOGE("Alert %d: %s", code, msg.c_str());
return Void();
}
};
个人经验:实现回调接口时,一定要保证方法体是轻量级的。因为 oneway 调用虽然不阻塞服务端,但回调方法是在 Binder 线程池里执行的。如果你在回调里做耗时操作,会阻塞其他回调的派发。我一般会在回调里把数据 post 到自己的工作线程去处理。
4.5 知识结构图
下面这张图总结了接口与回调的核心关系:
4.6 总结一下
接口与回调的设计,说白了就是回答两个问题:谁调谁?怎么调?
- 服务端接口:客户端调服务端,一般是同步调用,用于获取数据或下发命令。
- 回调接口:服务端调客户端,必须用 oneway,用于事件通知。
- oneway:发完即忘,不阻塞,适合高频通知。
- 同步:等待结果,可靠,适合查询和配置。
我个人习惯在写 .hal 文件时,先把所有接口按“查询/命令/通知”分类,再决定用同步还是 oneway。这样设计出来的接口,既清晰又不容易出性能问题。