3. HIDL 接口定义语言:语法、数据类型与规范

HIDL 这玩意儿,说白了就是 Android 系统里用来定义硬件接口的“契约语言”。你写好了 .hal 文件,客户端和服务端就得按这个契约来沟通。我刚开始接触 HIDL 的时候,觉得它跟 AIDL 长得挺像,但踩过几次坑之后才发现——细节上差别大了去了。

3.1 HIDL 语法基础

一个 .hal 文件,结构其实很清晰。我习惯把它拆成三块看:包声明、导入语句、接口定义。

// 包声明:反向域名,必须跟文件路径一致
package vendor.example.hardware.foo@1.0;

// 导入依赖的类型
import android.hardware.graphics.common@1.0::PixelFormat;

// 接口定义
interface IFoo {
    // 方法声明
    doSomething(uint32_t input) generates (Result result);
};

嗯,这里要注意:包路径必须跟文件系统路径完全对应。比如上面这个文件,就得放在 vendor/example/hardware/foo/1.0/IFoo.hal。我曾经见过有同事把版本号写错了,结果编译死活过不去,查了半天才发现是路径跟声明不匹配。

个人习惯:每次新建 .hal 文件,我都会先确认包路径,再写代码。路径错了,后面全是白费功夫。

3.2 数据类型详解

HIDL 的数据类型,我把它分成四类。你想想看,搞清楚了这四类,写接口基本不会卡壳。

类别 类型 说明
基础类型 uint8_t, int32_t, float, bool 等 跟 C++ 基本一致,长度固定
容器类型 vec<T>, string 动态数组和字符串
复合类型 struct, enum, union 自定义数据结构
句柄类型 handle, memory, fmq_sync 用于传递文件描述符、共享内存

这里我想重点说一下 vec<T>。它跟 C++ 的 std::vector 很像,但有个坑:HIDL 的 vec 不支持嵌套太深。我记得有一次,我在接口里定义了一个 vec<vec<vec<int32_t>>>,结果编译直接报错。后来查文档才知道,HIDL 限制嵌套最多三层。

避坑指南:我曾经在传递大块数据时用了 vec<uint8_t>,结果性能惨不忍睹。后来换成 memory 类型走共享内存,速度直接提升了一个数量级。记住:大数据走 memory,小数据走 vec。

3.3 包结构与版本管理

HIDL 的包结构,我建议你把它想象成一个“文件夹+版本号”的组合。每个包都是一个独立的目录,版本号用 @major.minor 表示。

vendor/example/hardware/foo/
├── 1.0/
│   ├── IFoo.hal
│   ├── types.hal
│   └── Android.bp
├── 2.0/
│   ├── IFoo.hal
│   ├── types.hal
│   └── Android.bp

版本号怎么定?我个人的经验是:

  • major 版本:接口不兼容时升级。比如删了方法、改了参数类型。
  • minor 版本:只增加不删除。比如加了个新方法,旧方法保持不变。

为什么要这么严格?因为 Android 的 Treble 架构要求 HAL 实现跟系统框架解耦。你改了一个接口,所有依赖它的模块都得跟着改。所以,能不升版本就别升,能加方法就别删方法。

核心原则:HIDL 接口一旦发布,就要保持向后兼容。这是 Treble 架构的基石,也是你作为 HAL 开发者的基本素养。

3.4 接口与方法定义规范

接口定义,说白了就是告诉别人“我能干什么”。每个方法都要明确输入、输出,以及是否会产生阻塞。

interface ICamera {
    // 同步方法:调用方会阻塞等待结果
    openCamera(uint32_t cameraId) generates (Status status);

    // 单向方法:调用方不等待返回
    oneway notifyCallback(CallbackData data);

    // 回调接口:服务端主动通知客户端
    interface ICallback {
        onFrameAvailable(uint32_t frameId);
    };
};

这里有几个规范,我建议你记住:

  • 方法名用驼峰命名:首字母小写,比如 openCamera,不要用下划线。
  • 参数名要有意义:别用 a、b、tmp 这种,别人看不懂。
  • 返回值用 generates 关键字:别用 return,那是 C++ 的写法。
  • 单向方法用 oneway 修饰:适用于日志、通知等不需要确认的场景。
我的习惯:每个接口文件开头都加一段注释,说明这个接口的用途、调用时机、以及线程安全要求。别嫌麻烦,三个月后你自己回来看代码,会感谢这段注释的。

3.5 HIDL 知识体系结构图

下面这张图,是我整理 HIDL 知识体系时画的。你可以把它当成一张“地图”,学完这章后对照着检查自己掌握了多少。

HIDL 接口定义语言知识体系 HIDL 接口定义语言 语法基础 数据类型 包结构与版本 接口定义规范 方法定义规范 包声明 导入语句 基础类型 容器类型 句柄类型 核心:包路径 = 文件路径,版本号决定兼容性 方法定义要明确同步/异步,参数要有意义

3.6 实战中的避坑指南

最后,我分享几个实战中遇到的坑,希望能帮你少走弯路。

  • 别用 string 传大文本:HIDL 的 string 本质上是 vec<uint8_t> 的封装,传大文本性能很差。我遇到过传 1MB 的 JSON 字符串,直接导致 ANR。后来改成 memory 走共享内存,问题解决。
  • 注意线程安全:HIDL 服务端的方法默认是在 Binder 线程池里执行的。如果你的实现里有共享数据,记得加锁。我曾经因为没加锁,导致两个客户端同时调用时数据错乱,查了整整两天。
  • 别滥用 oneway:单向方法虽然不阻塞,但也不保证送达。如果你需要确认对方收到了,就别用 oneway。我见过有人把所有方法都写成 oneway,结果丢消息了都不知道。
重要提醒:HIDL 接口一旦发布,就不要轻易修改。如果你非要改,记得升 major 版本,并且通知所有依赖方。否则,轻则编译报错,重则运行时崩溃。