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 后面跟的就是返回值类型。
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 的核心流程
为了让你更直观地理解,我画了一张流程图:
这张图把整个流程串起来了。你想想看,客户端调用 setBrightness(),实际上是在调用本地的代理对象。代理把参数打包,通过 Binder 驱动发给服务端。服务端解包后执行真正的硬件操作,再把结果传回来。
setBrightness() 里做了耗时的 I/O 操作,把 Binder 线程池占满了。解决办法是把耗时操作放到独立线程里,或者用异步回调。
3.5 版本管理
HIDL 的版本号是强制的。每次接口变更,版本号必须升级。比如从 @2.0 升到 @2.1。我建议你遵循这个原则:
- 小版本升级:只增加新方法,不修改旧方法。旧客户端还能用。
- 大版本升级:修改或删除旧方法。旧客户端必须适配。
实际项目中,我见过有人为了省事,直接改旧接口不升版本。结果系统 OTA 后,旧 HAL 实现和新 Framework 不兼容,设备直接变砖。嗯,这个教训够深刻。
好了,HIDL 的基础就讲到这里。你只要把 .hal 文件写好,数据类型选对,服务端和客户端的绑定流程跑通,后面的事情就顺了。