2. HAL 接口设计原则:抽象、封装、解耦,以及 HIDL 与 AIDL 的选择
这一章,我们来聊聊 HAL 接口设计的核心思想。说白了,就是怎么把硬件藏好,让上层用着舒服,底层改着方便。
我刚开始做 HAL 时,也踩过不少坑。最典型的就是把硬件寄存器直接暴露给上层。结果呢?换了个芯片,上层代码全得重写。后来我才明白,HAL 设计的本质就六个字:抽象、封装、解耦。
2.1 抽象:只暴露该暴露的
抽象,就是只告诉别人「你能做什么」,不告诉别人「你怎么做的」。比如一个摄像头 HAL,上层只需要知道 open()、startPreview()、takePicture(),至于底层是 I2C 还是 SPI,是 OV5640 还是 IMX219,上层根本不用关心。
我在项目中遇到过一件事:有个同事把 sensor 的增益计算方式直接写进了接口参数里。结果换了 sensor,接口签名都得改。这就是抽象没做好。
好的抽象应该像这样:
// 不好的抽象:暴露了实现细节
int setGain(int analog_gain, int digital_gain);
// 好的抽象:只暴露概念
int setExposureCompensation(int ev_value);
你想想看,上层应用真的需要知道模拟增益和数字增益的区别吗?不需要。它只需要知道「曝光补偿」这个抽象概念就够了。
2.2 封装:把复杂性关进笼子里
封装,就是把实现细节藏起来。上层调用接口时,不需要知道内部怎么运作的。
我记得有个项目,音频 HAL 的初始化函数有 12 个参数。每次调用都要传一堆 NULL 或 0。这就是封装没做好。
好的封装应该做到:
- 接口参数少:一般不超过 5 个
- 内部状态不外露:用句柄或指针隐藏实现
- 错误码统一:别一会儿返回 -1,一会儿返回 NULL
status_t。这样上层处理错误时,逻辑就统一了。
2.3 解耦:让各层各司其职
解耦,就是让 HAL 接口不依赖具体的硬件实现。你换硬件,接口不变;你换接口实现,上层不变。
我曾经见过一个项目,HAL 接口里直接传了 struct i2c_msg。这等于把驱动层的细节暴露给了框架层。后来换了 SPI 总线,整个 HAL 接口都得改。
解耦的关键在于:
- 接口不依赖硬件数据结构:用抽象的数据类型
- 接口不依赖特定协议:别把 I2C、SPI 的细节带进来
- 接口不依赖操作系统:尽量用标准类型
struct file_operations。结果项目要移植到 RTOS 上,整个 HAL 都得重写。从那以后,我坚持接口只使用标准 C 类型。
2.4 HIDL 与 AIDL 的选择
Android 提供了两种接口定义语言:HIDL 和 AIDL。很多人纠结选哪个。其实没那么复杂,我帮你理清楚。
| 对比维度 | HIDL | AIDL |
|---|---|---|
| 设计初衷 | HAL 层接口定义 | 应用层 IPC |
| 稳定性 | 接口稳定,版本化管理 | 接口灵活,可随时修改 |
| 性能 | 支持直通模式,零拷贝 | Binder 传输,有序列化开销 |
| 适用场景 | 硬件抽象层,跨进程 HAL | 应用与服务通信 |
| 学习成本 | 较高,需要理解 hidl-gen | 较低,Android 开发者都熟悉 |
我的建议很简单:
- 做 HAL 接口:用 HIDL。它天生就是为硬件抽象设计的,支持版本化、直通模式、零拷贝。
- 做应用层 IPC:用 AIDL。它更灵活,开发效率更高。
- 做系统服务:看情况。如果服务需要跨进程调用硬件,用 HIDL;如果只是应用层逻辑,用 AIDL。
我记得有个项目,团队用了 AIDL 做 HAL 接口。结果每次升级 Android 版本,接口都得跟着改。后来换成 HIDL,接口版本化管理,升级就轻松多了。
2.5 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
2.6 实战建议
最后,给你几个实战中的建议:
- 先设计接口,再写实现:我习惯先写 .hal 或 .aidl 文件,评审通过后再写 C++ 代码。
- 接口要小,职责要单一:一个接口只做一件事。别搞出万能接口。
- 版本化要早做:HIDL 的版本管理是强项,别等到接口变了才想起来。
- 性能敏感用 HIDL 直通模式:如果 HAL 和框架在同一个进程,直通模式可以省掉 Binder 开销。