3. HAL 接口定义语言 (HIDL) 基础

好,咱们今天聊聊 HIDL。说实话,我刚接触 Android 的时候,觉得这玩意儿就是个「中间人」,没啥了不起的。直到我在一个项目里,因为接口定义不规范,导致 Camera HAL 和 Framework 之间来回扯皮,调试了整整三天……嗯,从那以后,我再也不敢小看 HIDL 了。

HIDL,全称是 HAL Interface Definition Language。说白了,它就是用来定义 HAL 层接口的一种语言。你想想看,Framework 是 Java 写的,HAL 是 C++ 写的,两个世界怎么沟通?HIDL 就是那座桥。

3.1 HIDL 语法速览

HIDL 的语法其实挺简单的。我个人习惯把它看作是「带类型约束的 C++ 头文件」。它支持基本类型、结构体、枚举、接口等。

先看一个最简单的例子:

package android.hardware.camera@3.4;

interface ICameraDevice {
    /**
     * 打开相机设备
     * @return status 操作状态
     */
    open() generates (Status status);
    
    /**
     * 关闭相机设备
     */
    close();
    
    /**
     * 配置数据流
     * @param streams 数据流配置列表
     * @return status 操作状态
     */
    configureStreams(vec<Stream> streams) generates (Status status);
};

这里有几个关键点:

  • package:包名,后面跟的是版本号。版本号很重要,我见过有人因为版本号写错,编译死活过不去。
  • interface:接口定义。每个接口就是一个服务。
  • generates:表示这个方法有返回值。注意,HIDL 的方法返回值是异步的,通过 callback 返回。
  • vec<T>:向量类型,相当于 C++ 的 vector。

重要提示:HIDL 的版本号是递增的,不能回退。比如你发布了 3.4 版本,就不能再发布 3.3 版本。这是 Google 的硬性规定,为了保持向前兼容。

3.2 .hal 文件编写实战

写 .hal 文件,其实就是在定义「契约」。Framework 和 HAL 都要遵守这个契约。我在项目中遇到过最坑的事情,就是 .hal 文件里定义了一个方法,但 HAL 端没实现,结果 Framework 调用时直接 crash。

来看一个更完整的例子,定义一个 Camera 设备相关的接口:

package android.hardware.camera@3.4;

import android.hardware.camera.common@1.0::types;

interface ICameraDevice {
    /**
     * 初始化相机设备
     */
    init() generates (Status status);
    
    /**
     * 获取相机特性
     * @return characteristics 相机特性数据
     */
    getCameraCharacteristics() generates (CameraCharacteristics characteristics);
    
    /**
     * 设置输出流
     * @param outputStreams 输出流列表
     * @param configMode 配置模式
     */
    configureStreams(vec<Stream> outputStreams, 
                     StreamConfigurationMode configMode) 
        generates (Status status, 
                   vec<HalStream> halStreams);
    
    /**
     * 处理请求
     * @param request 捕获请求
     * @return sequenceId 序列号
     */
    processCaptureRequest(CaptureRequest request) 
        generates (Status status, 
                   int32_t sequenceId);
    
    /**
     * 刷新所有待处理请求
     */
    flush() generates (Status status);
};

写 .hal 文件时,有几个坑要注意:

  • 类型要明确:HIDL 是强类型的,int 就是 int32_t,别偷懒写 int。
  • 注释要写清楚:每个方法、每个参数都要有注释。我见过一个项目,.hal 文件里全是英文注释,结果印度同事看不懂,又重写了一遍。
  • 版本号要一致:package 里的版本号,要和 Android.mk 或 Android.bp 里的版本号一致。

我的小技巧:写 .hal 文件时,先画个接口图,把方法、参数、返回值都列出来。这样不容易漏掉东西。我曾经因为少定义了一个回调方法,导致整个 Camera HAL 无法正常工作,排查了整整两天。

3.3 服务端与客户端通信模型

HIDL 的通信模型,说白了就是「服务端提供接口,客户端调用接口」。但这里有个关键点:HIDL 是跨进程的。Framework 和 HAL 运行在不同的进程里,它们通过 Binder 通信。

通信流程大概是这样的:

  1. 服务端注册:HAL 服务启动后,会向 ServiceManager 注册自己。
  2. 客户端获取:Framework 通过 ServiceManager 获取 HAL 服务的代理。
  3. 方法调用:Framework 调用代理对象的方法,参数通过 Binder 传递到 HAL 端。
  4. 结果返回:HAL 端处理完后,通过 callback 或 generates 返回结果。

来看一个服务端的实现示例:

// CameraDevice.cpp
class CameraDevice : public ICameraDevice {
public:
    Return<Status> init() override {
        // 初始化硬件
        // 我习惯在这里加个日志,方便调试
        ALOGD("CameraDevice::init()");
        return Status::OK;
    }
    
    Return<void> close() override {
        // 关闭硬件
        ALOGD("CameraDevice::close()");
    }
    
    Return<Status, vec<HalStream>> configureStreams(
            const vec<Stream>& outputStreams,
            StreamConfigurationMode configMode) override {
        // 配置数据流
        // 这里要注意,outputStreams 可能为空
        if (outputStreams.size() == 0) {
            ALOGE("configureStreams: no output streams");
            return {Status::ILLEGAL_ARGUMENT, {}};
        }
        
        // 实际配置逻辑...
        vec<HalStream> halStreams;
        // ...
        return {Status::OK, halStreams};
    }
};

客户端的调用方式:

// CameraService.cpp
sp<ICameraDevice> cameraDevice = 
    ICameraDevice::getService("default");

// 调用方法
Status status = cameraDevice->init();
if (status != Status::OK) {
    ALOGE("Failed to init camera device");
    return;
}

// 配置数据流
vec<Stream> streams;
// ... 填充 streams
auto result = cameraDevice->configureStreams(streams, 
    StreamConfigurationMode::NORMAL);
if (result.isOk()) {
    Status s = result[0];
    vec<HalStream> halStreams = result[1];
    // 处理结果
}

注意:HIDL 的调用是同步的,但底层是通过 Binder 异步实现的。如果你在 HAL 端做耗时操作,比如 100ms 以上的操作,一定要用异步回调的方式,否则会阻塞 Framework 的主线程。我曾经因为一个 200ms 的初始化操作,导致整个系统 ANR,被测试小姐姐追着骂了一周。

3.4 避坑指南

做 HIDL 开发这几年,我踩过的坑不少,总结几个最常见的:

  • 版本号冲突:同一个包名不能有两个版本同时存在。如果你要升级版本,必须保证所有依赖都同步升级。
  • 接口不兼容:修改 .hal 文件后,一定要重新生成代码。我见过有人手动修改生成的代码,结果编译报错,排查了半天才发现是手改的。
  • 内存泄漏:HIDL 的跨进程调用,对象是序列化传输的。如果你在 HAL 端 new 了一个对象,记得在 Framework 端 delete。否则,内存泄漏是迟早的事。
  • 死锁问题:HIDL 的回调是在 Binder 线程中执行的。如果你在回调里又调用了 HAL 的方法,可能会造成死锁。我建议回调里只做简单的状态更新,不要做复杂操作。

核心要点:HIDL 的本质是「契约」。写好 .hal 文件,就是定好契约。服务端和客户端都要严格遵守这个契约。任何一方的违约,都会导致系统崩溃。所以,写 .hal 文件时,一定要想清楚每个方法、每个参数、每个返回值。

好了,HIDL 的基础就聊这么多。下一章,咱们会深入 HIDL 的高级特性,比如回调、死锁检测、性能优化等。到时候我会分享更多实战中的坑和技巧。