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 知识结构图

下面这张图总结了接口与回调的核心关系:

接口与回调调用关系图 HIDL 接口与回调调用关系 客户端 实现回调接口 服务端 实现主接口 同步调用(等待返回值) oneway 回调(不等待) 关键设计原则 • 服务端接口:同步调用,用于查询和配置 • 回调接口:oneway 调用,用于事件通知 • 避免在回调中做耗时操作,防止阻塞 Binder 线程池 Binder 驱动 序列化 反序列化

4.6 总结一下

接口与回调的设计,说白了就是回答两个问题:谁调谁?怎么调?

  • 服务端接口:客户端调服务端,一般是同步调用,用于获取数据或下发命令。
  • 回调接口:服务端调客户端,必须用 oneway,用于事件通知。
  • oneway:发完即忘,不阻塞,适合高频通知。
  • 同步:等待结果,可靠,适合查询和配置。

我个人习惯在写 .hal 文件时,先把所有接口按“查询/命令/通知”分类,再决定用同步还是 oneway。这样设计出来的接口,既清晰又不容易出性能问题。