1. 从应用层到内核层之间,HAL 到底卡在什么位置
很多人学 Android 框架,学着学着就卡在 HAL 这一层了。往上看,应用框架层(Application Framework)的 ActivityManager、WindowManager 还能靠读源码和调试理解个大概;往下看,Linux 内核的驱动模型、字符设备、sysfs 也有大量现成资料。唯独夹在中间的 HAL(Hardware Abstraction Layer,硬件抽象层),资料零散、版本差异大、各家实现又不公开,学着特别容易一头雾水。
我自己刚开始接触 HAL 的时候,最大的困惑就是:既然内核已经有驱动了,为什么还要在用户空间再搞一层 HAL?直接让 Framework 调驱动不行吗?后来做了一些涉及传感器、音频、相机模块的适配工作,才慢慢把这条链路理顺。这篇内容就是把我对 Android HAL 的理解完整拆开讲一遍,从它为什么存在、经历了哪几代演进、AIDL 化之后长什么样,到实际动手写一个 HAL 模块需要哪些步骤、容易踩哪些坑。
适合的读者是:已经了解 Android 四大组件和基本框架、想往系统层深入的人;做系统定制、ROM 适配、驱动对接的工程师;以及准备面试系统岗、被 HAL 相关问题问住过的人。我会尽量用生活化的类比把抽象概念讲清楚,同时把关键机制和实操细节补足,让你看完能自己动手跑通一个最小 HAL 模块。
先给一个最直观的定位:如果把 Android 系统比作一家餐厅,内核驱动是后厨里真正炒菜的灶台,应用框架是前台点菜的服务员,那 HAL 就是中间那个“翻译+标准化”的岗位——前台不需要知道灶台是燃气还是电磁,只需要按统一菜单下单;后厨也不需要管前台用什么点单系统,只管按标准接口出菜。HAL 就是这套“标准菜单”和“标准出菜口”。
2. HAL 存在的真正理由:不只是“多一层”
2.1 许可证隔离:GPL 与 Apache 的那道墙
要理解 HAL 的诞生,得先回到法律层面。Linux 内核采用 GPLv2 许可证,这意味着任何直接链接进内核的代码,理论上都要以 GPL 兼容的方式开源。但 Android 早期想让大量硬件厂商的驱动实现保持闭源,因为这里面往往包含厂商的核心竞争力和商业机密。
如果厂商把驱动直接写进内核,就得开源;如果不开源,就没法合规地进内核。这个矛盾怎么解?答案就是把硬件相关的逻辑从内核搬到用户空间,用 Apache License 2.0(后来是 Apache 2.0)这种宽松许可证来发布。内核里只留一个极简的、通用的驱动接口(比如通过字符设备暴露几个读写节点),真正的硬件控制逻辑放在用户空间的 HAL 里。这样内核部分可以开源,HAL 部分厂商可以闭源,两边都合规。
注意:这个“许可证隔离”是 HAL 最原始、最核心的动机,很多讲 HAL 的文章只讲“抽象硬件差异”,其实没说到点子上。抽象差异是结果,许可证隔离才是起因。
2.2 硬件差异的收敛:让 Framework 只认接口不认芯片
第二个理由才是大家常说的“抽象”。市面上的硬件千差万别:同样是摄像头,有的用某厂商的传感器,有的用另一家的;同样是音频,编解码芯片、功放、通路设计各不相同。如果 Framework 层要为每一种硬件写一套适配代码,那代码量会爆炸,而且每出一款新硬件就要改 Framework,完全不现实。
HAL 的做法是定义一组标准接口(比如hardware/libhardware/include/hardware/camera.h里定义的camera_device_t),Framework 只调用这些接口,具体实现由各厂商的 HAL 模块提供。Framework 说“给我拍一张照”,HAL 负责翻译成“给某型号传感器发某个寄存器命令”。Framework 永远不需要知道底下是什么芯片。
这就像 USB 接口:你的电脑不需要知道插上来的是哪个牌子的 U 盘,只要它符合 USB 规范就能用。HAL 就是硬件世界的“USB 规范”。
2.3 进程隔离与稳定性:HAL 崩了不该拖垮系统
第三个理由常被忽略,但很重要:进程隔离。早期的 HAL 是直接加载进使用它的进程(比如某个系统服务)的地址空间的,也就是所谓的“直通模式”(passthrough)。这种模式下,HAL 代码里的一个空指针解引用,可能直接把系统服务搞崩。
后来的 HAL 逐渐走向独立进程(binderized 模式),每个 HAL 模块跑在自己的进程里,通过 Binder(后来是 AIDL)通信。这样即使某个 HAL 崩溃,也只是它自己挂掉,Framework 可以重启它,不至于整个系统服务跟着陪葬。这个演进方向是理解现代 HAL 的关键。
3. HAL 的三代演进:从直通到 AIDL 的完整脉络
3.1 第一代:Legacy HAL 与 hw_get_module
最早的 HAL 用的是hw_get_module这套机制。Framework 通过一个模块 ID(比如CAMERA_HARDWARE_MODULE_ID)去查找对应的.so库,加载后拿到一个hw_module_t结构体,再从中取出具体设备结构体(如camera_device_t)。
这套机制的核心是hw_module_t和hw_device_t两个结构体,定义在hardware/libhardware/include/hardware/hardware.h里。厂商实现时,要导出一个符号HAL_MODULE_INFO_SYM,里面填好模块信息。Framework 用dlopen打开.so,用dlsym找到这个符号。
// 简化的模块定义结构 struct hw_module_t { uint32_t tag; uint16_t module_api_version; uint16_t hal_api_version; const char *id; const char *name; const char *author; struct hw_module_methods_t* methods; void* dso; // ... };这套机制的问题很明显:所有 HAL 都跑在调用者的进程里,没有隔离;接口是 C 结构体,版本管理靠手动维护module_api_version,容易出错;而且不同 HAL 之间没有统一的生命周期管理。
3.2 第二代:HIDL 与 Binder 化
Android 8.0(Oreo)引入了 HIDL(HAL Interface Definition Language),这是一次大重构。HIDL 用.hal文件定义接口,工具链自动生成 C++ 或 Java 的桩代码,底层走 Binder IPC。HAL 模块可以跑在独立进程里,Framework 通过 Binder 调用。
HIDL 解决了几个问题:接口有了正式的 IDL 描述,版本管理更规范(通过@1.0、@1.1这样的包版本);进程隔离到位;支持直通和绑定两种模式,过渡平滑。
但 HIDL 也有自己的包袱:它是一套 Android 特有的东西,学习成本高,工具链复杂,而且和 AIDL 并存导致生态割裂。很多开发者抱怨“又要学一套新 DSL”。
3.3 第三代:AIDL HAL 与统一接口
Android 11 开始,Google 推动用 AIDL 替代 HIDL。到 Android 13 及以后,新写的 HAL 基本都要求用 AIDL。AIDL 本来就是 Android 应用层熟悉的接口描述语言,复用它意味着开发者不需要再学一套新东西,工具链也统一了。
AIDL HAL 的接口定义放在hardware/interfaces/目录下,按功能分目录,比如hardware/interfaces/vibrator/aidl/。生成的代码走 Binder,HAL 跑在独立进程,通过servicemanager注册和发现。
三代演进的对比可以看这张表:
| 维度 | Legacy HAL | HIDL | AIDL HAL |
|---|---|---|---|
| 引入版本 | Android 早期 | Android 8.0 | Android 11+ |
| 接口描述 | C 结构体 | .hal 文件 | .aidl 文件 |
| 通信方式 | 直接函数调用 | Binder | Binder |
| 进程模型 | 同进程 | 可独立进程 | 独立进程 |
| 版本管理 | 手动 | 包版本 | 接口版本 |
| 现状 | 逐步淘汰 | 维护中 | 主推方向 |
理解这条演进线,你就明白为什么现在搜 HAL 资料会看到三种完全不同的写法——因为它们分属不同时代。
4. AIDL HAL 的目录结构与接口定义实战
4.1 一个 HAL 模块的目录长什么样
以振动器(vibrator)为例,AIDL HAL 的典型目录结构是这样的:
hardware/interfaces/vibrator/aidl/ ├── Android.bp ├── android/ │ └── hardware/ │ └── vibrator/ │ ├── IVibrator.aidl │ ├── IVibratorCallback.aidl │ └── Effect.aidl └── default/ ├── Android.bp ├── Vibrator.cpp └── Vibrator.handroid/hardware/vibrator/下面放接口定义(.aidl文件),default/下面是默认实现。这种“接口与实现分离”的布局是 AIDL HAL 的标准做法,接口可以被多个实现复用。
4.2 接口定义:IVibrator.aidl 拆解
一个简化的振动器接口定义大概是这样:
package android.hardware.vibrator; @VintfStability interface IVibrator { int getCapabilities(); void on(int timeoutMs, IVibratorCallback callback); void off(); boolean supportsAmplitudeControl(); void setAmplitude(int amplitude); }几个关键点值得说:
@VintfStability注解是必须的。VINTF(Vendor Interface)是 Android 用来管理 Framework 和 Vendor 之间接口稳定性的机制。加了@VintfStability的接口才会被纳入 VINTF 兼容性检查,保证系统升级时接口不会随便变。忘了加这个注解,编译可能过,但运行时servicemanager找不到服务。
package名要和目录结构对应。android.hardware.vibrator对应android/hardware/vibrator/这个路径,这是 AIDL 的硬性约定,写错了编译直接报错。
方法签名里用到的自定义类型(比如Effect)也要定义成@VintfStability的 parcelable 或 enum。
4.3 实现类:从 BnVibrator 继承
接口定义好后,工具链会生成BnVibrator(Binder Native 桩)。实现类继承它:
class Vibrator : public BnVibrator { public: ndk::ScopedAStatus getCapabilities(int32_t* _aidl_return) override; ndk::ScopedAStatus on(int32_t timeoutMs, const std::shared_ptr<IVibratorCallback>& callback) override; ndk::ScopedAStatus off() override; // ... };注意返回类型是ndk::ScopedAStatus,这是 AIDL HAL 里表示调用结果的标准类型,可以携带错误码。不要用传统的status_t或返回int,那样不符合 AIDL 规范。
4.4 注册服务:main 函数怎么写
HAL 实现要跑起来,需要一个入口把它注册到servicemanager:
int main() { ABinderProcess_setThreadPoolMaxThreadCount(0); std::shared_ptr<Vibrator> vibrator = ndk::SharedRefBase::make<Vibrator>(); const std::string instance = std::string() + Vibrator::descriptor + "/default"; binder_status_t status = AServiceManager_addService( vibrator->asBinder().get(), instance.c_str()); if (status != STATUS_OK) { return -1; } ABinderProcess_joinThreadPool(); return 0; }AServiceManager_addService把服务注册上去,实例名通常是接口名/default。Framework 侧通过AServiceManager_waitForService或getService拿到这个服务。
提示:实例名里的
default不是随便起的,它对应 VINTF manifest 里声明的实例。如果 manifest 里写的是default,代码里也必须用default,否则 Framework 找不到。
5. 编译系统与 VINTF:让 HAL 被系统“认出来”
5.1 Android.bp 里必须写对的三件事
AIDL HAL 的Android.bp通常包含两部分:接口库和实现库。接口库用aidl_interface定义:
aidl_interface { name: "android.hardware.vibrator", srcs: ["android/hardware/vibrator/*.aidl"], stability: "vintf", backend: { cpp: { enabled: true }, java: { enabled: true }, ndk: { enabled: true }, }, versions: ["1"], }stability: "vintf"是关键,它告诉编译系统这个接口要纳入 VINTF 稳定性管理。versions声明接口版本,后续加方法要升版本号。
实现库用cc_binary:
cc_binary { name: "android.hardware.vibrator-service.example", init_rc: ["vibrator-default.rc"], vendor: true, relative_install_path: "hw", srcs: ["default/Vibrator.cpp"], shared_libs: [ "libbase", "libbinder_ndk", "android.hardware.vibrator-V1-ndk", ], }vendor: true表示这是 Vendor 分区的东西,relative_install_path: "hw"决定它装到/vendor/bin/hw/下。
5.2 init.rc:服务怎么被拉起来
HAL 服务需要一个.rc文件让 init 进程管理:
service vendor.vibrator-default /vendor/bin/hw/android.hardware.vibrator-service.example class hal user system group systemclass hal让它归入 hal 类别,系统启动时统一拉起。user system指定运行身份。这个文件通常放在vendor/etc/init/下。
5.3 VINTF manifest:声明“我提供这个服务”
光注册服务还不够,系统要知道这个 HAL 的存在和版本。这就要在 VINTF manifest 里声明:
<hal format="aidl"> <name>android.hardware.vibrator</name> <version>1</version> <fqname>IVibrator/default</fqname> </hal>fqname就是前面注册时用的实例名。Framework 侧通过 VINTF 查询到有这么一个 HAL,才会去servicemanager找它。manifest 和实际注册的实例名对不上,是新手最常踩的坑之一。
5.4 兼容性检查:为什么编译过了运行还是挂
Android 有一套checkvintf工具,在启动时校验 Framework 期望的接口和 Vendor 提供的接口是否匹配。如果 Framework 要IVibrator/default版本 2,而 Vendor 只提供了版本 1,启动就会报兼容性错误。
排查这类问题,可以手动跑:
adb shell checkvintf它会列出所有不匹配的接口。实测下来,大部分“HAL 服务起来了但 Framework 用不了”的问题,都能在这里找到线索。
6. 调试 HAL 的实战手法与常见坑
6.1 确认服务到底有没有起来
第一步永远是确认服务在不在:
adb shell service list | grep vibrator如果列表里没有,说明服务没注册成功。这时候去看 logcat:
adb logcat | grep -i vibrator常见原因:AServiceManager_addService返回非 OK(实例名冲突或权限问题)、.rc文件没被加载、二进制没装到正确路径。
6.2 用 lshal 看 HAL 全貌
lshal是调试 HAL 的利器,它会列出系统里所有 HAL 服务及其状态:
adb shell lshal输出里能看到每个 HAL 的接口名、实例名、PID、客户端数量。如果某个 HAL 显示N/A或者没有 PID,说明它没跑起来。lshal还能看某个 HAL 的详细接口信息:
adb shell lshal debug android.hardware.vibrator.IVibrator/default6.3 直通模式与绑定模式的混淆
AIDL HAL 理论上都是绑定模式(独立进程),但有些老代码或特殊场景会用直通模式(同进程)。如果你在代码里看到AServiceManager_isDeclared之类的判断,或者实现里直接new出对象而不是注册服务,那可能是直通模式。
混淆这两种模式会导致调试方向完全错。判断方法:看进程。ps -A | grep vibrator,如果有独立进程,就是绑定模式;如果没有,可能是直通。
6.4 权限与 SELinux:最隐蔽的坑
HAL 服务跑在独立进程,涉及 Binder 通信、文件访问、设备节点操作,每一项都受 SELinux 策略约束。服务起来了但一调用就失败,十有八九是 SELinux 拒绝。
排查方法:
adb shell dmesg | grep avc或者:
adb logcat | grep avc会看到类似avc: denied { call } for ...的记录。根据拒绝的类型,去system/sepolicy/下对应的.te文件里加规则。比如 Binder 调用被拒,要加binder_call;访问设备节点被拒,要加rw_file_perms。
注意:SELinux 策略改动后要重新编译
sepolicy并刷入,不是改完就生效。而且策略要加在正确的域里,加错域等于没加。
6.5 接口版本升级的坑
给已有 HAL 加方法时,必须升版本号。比如从IVibrator加一个getVibrationStrength,要在aidl_interface的versions里加"2",并在新版本目录下放新的.aidl。老版本目录保持不变,保证向后兼容。
如果直接改老版本的.aidl,VINTF 检查会报“接口哈希不匹配”,系统可能起不来。这个坑很隐蔽,因为编译能过,问题在运行时才暴露。
7. 从零跑通一个最小 HAL 的完整步骤
7.1 环境与前置条件
假设你已经有 AOSP 源码树,能编译hardware/interfaces/下的模块。需要确认:
- 源码版本是 Android 13 或更新(AIDL HAL 成熟)
- 能跑
m命令编译单个模块 - 有可刷机的设备或模拟器
7.2 创建接口定义
在hardware/interfaces/下新建目录,比如hello/aidl/,按标准结构放.aidl文件:
// android/hardware/hello/IHello.aidl package android.hardware.hello; @VintfStability interface IHello { String greet(String name); }7.3 写实现与注册
实现类继承BnHello,实现greet方法,在main里注册服务。这部分和前面振动器的例子结构一致,照搬即可。
7.4 配置编译与 VINTF
写好Android.bp、.rc、VINTF manifest 片段。manifest 片段通常加到设备的device.mk或专门的vintf_fragments里。
7.5 编译、刷入、验证
m android.hardware.hello-service.example编译产物在out/target/product/<device>/vendor/bin/hw/下。刷入后:
adb shell lshal | grep hello adb shell service list | grep hello能看到服务就成功了一半。再写个简单的客户端调用greet,验证功能。
7.6 实测中的意外情况
我第一次跑通的时候,服务注册成功但客户端调不到。查了半天发现是 VINTF manifest 里的fqname写成了IHello/default,而代码里注册的是android.hardware.hello.IHello/default——少了包名前缀。这种细节错误编译不报,只能靠lshal和checkvintf发现。
另一个常见问题是.rc文件里的服务名和二进制名不一致,导致 init 找不到可执行文件。.rc里service后面跟的名字是服务名(自定义),再后面才是二进制路径,两者别搞混。
8. 我对 HAL 学习路径的一点个人体会
回头看,HAL 之所以难学,不是因为概念本身多复杂,而是因为它的资料被三代演进割裂了。你在网上搜到的教程,可能一半是 Legacy HAL 的hw_get_module,一半是 HIDL 的.hal文件,真正讲 AIDL HAL 的又往往只讲接口不讲 VINTF 和 SELinux。结果就是照着做,每一步都差一点,最后跑不起来。
我的建议是:先锁定一个版本(推荐 Android 13+),只学 AIDL HAL 这一套,把 Legacy 和 HIDL 当成历史背景了解即可。然后找一个最简单的 HAL(振动器、LED、传感器都行),从接口定义一路做到客户端调用,把整条链路走通一遍。走通之后再看其他 HAL,会发现套路完全一样,只是接口方法不同。
最后分享一个排查顺序,遇到 HAL 问题按这个顺序查,基本能覆盖九成情况:先lshal看服务在不在,再service list看注册名对不对,再logcat看有没有异常,再dmesg | grep avc看 SELinux,最后checkvintf看兼容性。这个顺序是从外到内、从粗到细,能帮你快速缩小问题范围,不至于一上来就扎进代码里瞎找。