3. HIDL 基础与实战:接口定义语言详解

HIDL,全称是 HAL Interface Definition Language。说白了,它就是 Android 用来定义硬件接口的一种“契约语言”。

我刚开始接触 HIDL 时,觉得它跟 AIDL 长得挺像。但用久了才发现,HIDL 是为底层硬件量身定做的。它要解决的问题很明确:让 Framework 和 HAL 实现解耦。你想想看,没有这个契约,系统升级了,驱动也得跟着改,那得多痛苦。

3.1 .hal 文件的编写规范

每个 .hal 文件,都定义了一个接口。我习惯把接口名写得尽量清晰,比如 IBrightness、IVibrator。这样一看就知道是干什么的。

先看一个最简单的例子:

// types.hal
package android.hardware.brightness@2.0;

enum BrightnessRange : uint32_t {
    MIN = 0,
    MAX = 255,
    DEFAULT = 128
};

// IBrightness.hal
package android.hardware.brightness@2.0;

interface IBrightness {
    setBrightness(uint32_t value) generates (bool success);
    getBrightness() generates (uint32_t value);
};

嗯,这里要注意几点:

  • 包名必须唯一:格式是 package vendor.xxx.hardware.xxx@版本号。我见过有人把包名写错,结果编译死活过不去。
  • 接口名以 I 开头:这是约定俗成的规矩,别打破它。
  • 方法返回值用 generates:HIDL 的方法可以同步返回,也可以异步回调。generates 后面跟的就是返回值类型。
我的习惯:每个 .hal 文件只定义一个接口。别把一堆接口塞进一个文件里,后期维护会想骂人。

3.2 数据类型与自定义类型

HIDL 支持的数据类型,比 AIDL 要丰富一些。我整理了一张表,方便你对照:

类别 类型 说明
基础类型 int8_t, uint8_t, int32_t, uint32_t, int64_t, uint64_t 固定宽度整数,跨平台安全
浮点类型 float, double IEEE 754 标准
字符串 string UTF-8 编码,长度不限
数组 T[] 可变长度数组,例如 uint8_t[]
枚举 enum 底层是整数,可以指定宽度
结构体 struct 可以嵌套,支持默认值
联合体 union 小心使用,容易踩坑
安全回调 callback 用于异步通知,类似接口回调

自定义类型也很简单。比如定义一个传感器数据:

struct SensorEvent {
    uint64_t timestamp;  // 纳秒时间戳
    float[] values;      // 传感器数值
    int32_t accuracy;    // 精度等级
};
我曾经踩过的坑:在结构体里用了 string 类型,结果跨进程传输时性能很差。后来改成固定长度的 uint8_t[],速度快了不止一倍。HIDL 的 string 在底层会触发内存分配,能不用就别用。

3.3 服务端与客户端的生成与绑定

写好 .hal 文件后,下一步就是生成代码。Android 的编译系统会自动帮你搞定。你只需要在 Android.bp 里声明一下:

hidl_interface {
    name: "android.hardware.brightness@2.0",
    root: "android.hardware",
    srcs: [
        "types.hal",
        "IBrightness.hal",
    ],
    interfaces: [
        "android.hidl.base@1.0",
    ],
    gen_java: true,
    gen_java_constants: true,
};

编译后,你会得到一堆自动生成的 C++ 和 Java 文件。服务端和客户端的骨架代码都有了。

3.3.1 服务端实现

服务端就是 HAL 实现方。你需要继承生成的 BnHw 类:

#include <android/hardware/brightness/2.0/IBrightness.h>

using namespace android::hardware::brightness::V2_0;

class BrightnessImpl : public IBrightness {
public:
    Return<bool> setBrightness(uint32_t value) override {
        // 这里写真正的硬件操作
        // 比如写寄存器、调 PWM 等
        return true;
    }

    Return<uint32_t> getBrightness() override {
        // 读取当前亮度值
        return 128;
    }
};

然后注册服务:

int main() {
    sp<IBrightness> service = new BrightnessImpl();
    status_t status = service->registerAsService("default");
    if (status != OK) {
        // 注册失败,处理错误
    }
    // 进入事件循环
    joinRpcThreadpool();
    return 0;
}

3.3.2 客户端绑定

客户端就是 Framework 层。它通过服务名来获取代理:

sp<IBrightness> service = IBrightness::getService("default");
if (service == nullptr) {
    // 服务没启动,或者版本不匹配
    return;
}

// 调用方法
bool success;
Return<bool> ret = service->setBrightness(200);
if (ret.isOk()) {
    success = ret;
} else {
    // 远程调用失败
}
关键点:客户端获取服务时,默认是同步等待的。如果服务端挂了,客户端会卡住。我建议用 getService() 的异步版本,或者设置超时。

3.4 HIDL 的核心流程

为了让你更直观地理解,我画了一张流程图:

HIDL 服务端与客户端交互流程 HIDL 服务端与客户端交互流程 客户端 (Framework) 服务端 (HAL 实现) HIDL 代理 (Bp/Bn) 调用接口方法 序列化传输 返回结果 反序列化 说明: 1. 客户端通过服务名获取代理 (Bp) 2. 代理将方法调用序列化为 HIDL 协议数据 3. 服务端 (Bn) 反序列化并调用实际实现 4. 结果再序列化返回给客户端

这张图把整个流程串起来了。你想想看,客户端调用 setBrightness(),实际上是在调用本地的代理对象。代理把参数打包,通过 Binder 驱动发给服务端。服务端解包后执行真正的硬件操作,再把结果传回来。

避坑指南:我曾经在调试时发现,客户端调用总是超时。查了半天,原来是服务端在 setBrightness() 里做了耗时的 I/O 操作,把 Binder 线程池占满了。解决办法是把耗时操作放到独立线程里,或者用异步回调。

3.5 版本管理

HIDL 的版本号是强制的。每次接口变更,版本号必须升级。比如从 @2.0 升到 @2.1。我建议你遵循这个原则:

  • 小版本升级:只增加新方法,不修改旧方法。旧客户端还能用。
  • 大版本升级:修改或删除旧方法。旧客户端必须适配。

实际项目中,我见过有人为了省事,直接改旧接口不升版本。结果系统 OTA 后,旧 HAL 实现和新 Framework 不兼容,设备直接变砖。嗯,这个教训够深刻。

好了,HIDL 的基础就讲到这里。你只要把 .hal 文件写好,数据类型选对,服务端和客户端的绑定流程跑通,后面的事情就顺了。