1. 为什么现在还要重新理解一遍 AIDL HAL
如果你最近两三年才开始接触 Android 系统开发,可能会有一个错觉:HAL 不就是那个hardware/libhardware下面一堆hw_get_module和hw_module_t结构体吗?写个 C 文件,填几个函数指针,编译成.so丢进/vendor/lib/hw就完事了。这个印象在 Android 8.0 之前基本成立,但从 Android 8.0 引入 Treble 架构、Android 11 开始强制要求新 HAL 使用 AIDL 之后,整个 HAL 的写法、调试方式、甚至思维方式都变了。
我真正被 AIDL HAL 折腾,是在给一块自研板子做传感器适配的时候。当时按照老思路写了个sensors.default.so,结果dumpsys sensorservice里死活看不到设备,logcat 里只有一句冷冰冰的Cannot find module。后来才发现,Android 11 之后很多新平台已经不再走 legacy HAL 那条路了,必须用 AIDL 定义接口、用hidl2aidl或者手写 service、注册到servicemanager,再通过 VINTF 声明才能被 framework 发现。这一套流程走下来,坑比想象中多得多。
所以这篇内容我想做的事情很明确:把 AIDL HAL 从零到一完整走一遍。不是那种只贴几段代码的教程,而是把每一步背后的“为什么”讲清楚——为什么接口要这么定义、为什么 service 要这么注册、为什么 VINTF 清单少一行就起不来。适合已经会写 C++、懂一点 Android 编译系统、但被 AIDL HAL 卡住的开发者。如果你连Android.bp都没写过,建议先补一下 Soong 构建系统的基础,不然中间会有点吃力。
关键词里出现了aidl文件生成失败、android aidl所有的考点、hal文件、hal库文件结构这些,说明很多人卡在接口定义和文件组织上。我会把这些高频痛点穿插在对应章节里,尽量让你少走我当年走过的弯路。
2. AIDL HAL 和 Legacy HAL 到底差在哪
2.1 从 hw_module_t 到 Binder 通信的范式转移
Legacy HAL 的本质是“动态库加载 + 函数指针调用”。Framework 通过hw_get_module找到对应的.so,拿到hw_module_t,再通过open拿到hw_device_t,之后所有调用都是同一个进程内的函数调用。这种方式简单直接,但问题也很明显:HAL 和 framework 跑在同一个进程里,HAL 崩了整个 system_server 跟着挂;而且 HAL 和 framework 的编译耦合很紧,升级 framework 往往要重新编译 HAL。
AIDL HAL 把这一切改成了 Binder IPC。HAL 变成一个独立的 service 进程,framework 通过 Binder 代理去调用它。这样一来,HAL 崩溃不会直接拖垮 framework,两者可以独立升级,接口通过.aidl文件严格定义,编译期就能发现不匹配。代价是通信开销变大,而且多了一整套 service 注册、VINTF 声明、SELinux 策略的配置工作。
我个人的判断是:如果你做的是新项目,尤其是 Android 11 以上的平台,不要犹豫,直接上 AIDL HAL。Legacy HAL 虽然还能用,但 Google 已经在逐步收紧,很多新接口只提供 AIDL 版本,硬扛 legacy 只会让自己越来越被动。
2.2 三种 HAL 形态的横向对比
为了让你有个直观感受,我把 legacy HAL、HIDL HAL、AIDL HAL 放在一起对比一下。这张表是我自己在选型时整理的,实际项目里很有参考价值。
| 维度 | Legacy HAL | HIDL HAL | AIDL HAL |
|---|---|---|---|
| 通信方式 | 同进程函数调用 | Binder IPC | Binder IPC |
| 接口定义 | 头文件 + 结构体 | .hal文件 | .aidl文件 |
| 进程隔离 | 无 | 有 | 有 |
| 独立升级 | 困难 | 支持 | 支持 |
| Android 版本 | 8.0 前主流 | 8.0-11 主流 | 11 后推荐 |
| 调试工具 | logcat | lshal | lshal + aidl 工具 |
| 学习曲线 | 低 | 中 | 中高 |
从表里能看出来,AIDL HAL 并不是凭空冒出来的,它是 HIDL 的继任者。Google 在 Android 11 之后把 HIDL 标记为 deprecated,新 HAL 一律用 AIDL。所以如果你现在还在纠结学 HIDL 还是 AIDL,答案很明确:学 AIDL,HIDL 只需要能看懂老代码就行。
2.3 一个容易被忽略的点:AIDL HAL 不等于应用层 AIDL
这里有个概念上的坑,我见过不少从应用层转过来的开发者会混淆。应用层的 AIDL 是用来做进程间通信的,比如你写个IRemoteService.aidl,客户端 bindService 之后拿到代理调用。AIDL HAL 虽然也用.aidl文件,但它的运行环境、注册方式、发现机制完全不同。
应用层 AIDL 通过ServiceManager或者bindService来获取,而 AIDL HAL 是通过servicemanager注册,framework 侧通过IServiceManager::getService拿到。更关键的是,AIDL HAL 的 service 必须声明在 VINTF 清单里,否则lshal根本看不到它。这个差异导致很多应用层 AIDL 的经验在 HAL 场景下不适用,比如你不能用bindService去连一个 HAL service。
3. 动手之前:接口定义与文件结构怎么规划
3.1 .aidl 文件放在哪,包名怎么起
AIDL HAL 的接口文件通常放在hardware/interfaces/下面,按照hardware/interfaces/<模块名>/aidl/的路径组织。比如你要做一个自定义的传感器 HAL,可以放在hardware/interfaces/mysensor/aidl/android/hardware/mysensor/下面。包名一般遵循android.hardware.<模块名>的约定,这样 framework 侧查找的时候不容易出错。
我踩过的一个坑是包名和目录结构不一致。AIDL 编译器对包名和路径的对应关系要求很严格,package android.hardware.mysensor;对应的文件必须放在android/hardware/mysensor/目录下,否则编译时会报aidl文件生成失败。这个错误信息很模糊,第一次遇到的时候我查了半天才发现是目录层级少了一层。
一个典型的接口文件长这样:
// hardware/interfaces/mysensor/aidl/android/hardware/mysensor/IMySensor.aidl package android.hardware.mysensor; @VintfStability interface IMySensor { int getValue(); void setThreshold(int threshold); String getSensorName(); }注意那个@VintfStability注解,这是 AIDL HAL 特有的。加上它之后,接口会被标记为 VINTF 稳定,才能被 framework 通过 VINTF 机制发现。如果忘了加,service 能起来,但 framework 找不到,lshal里也看不到。这个注解是很多人第一次写 AIDL HAL 时最容易漏掉的东西。
3.2 数据类型的选择:哪些能用,哪些要绕道
AIDL HAL 支持的数据类型比应用层 AIDL 要严格一些。基本类型int、long、boolean、float、double、String都没问题,数组用int[]这种形式,复杂结构体需要单独定义.aidl文件并用parcelable声明。
但有几个坑要注意。第一,AIDL HAL 里不要用List、Map这些集合类型,虽然语法上支持,但在 VINTF 稳定接口里会带来兼容性问题。第二,自定义的 parcelable 结构体必须显式实现Parcelable,而且字段顺序要和.aidl声明一致,否则跨进程传输时会解析错位。第三,枚举类型要用@Backing(type="int")注解,不然编译不过。
我建议的做法是:能用基本类型就用基本类型,复杂数据尽量拆成多个简单字段。这样虽然接口看起来啰嗦一点,但稳定性和可调试性好很多。当年我为了图省事在一个接口里塞了个嵌套结构体,结果后面改字段的时候发现 VINTF 兼容性检查过不了,只能重新设计接口,返工成本很高。
3.3 Android.bp 怎么写才不出错
接口定义好之后,需要写Android.bp来告诉构建系统怎么编译。一个典型的 AIDL HAL 接口的Android.bp大概是这样:
aidl_interface { name: "android.hardware.mysensor", vendor_available: true, srcs: ["android/hardware/mysensor/*.aidl"], stability: "vintf", backend: { cpp: { enabled: true, }, java: { enabled: false, }, }, versions: ["1"], }这里有几个关键字段。vendor_available: true让接口对 vendor 分区可见,stability: "vintf"对应前面说的@VintfStability,backend里按需开启 C++ 或 Java 后端。versions是接口版本号,第一次写["1"]就行,后面接口有变更时再加新版本。
我遇到过的aidl文件生成失败里,有一半以上是Android.bp配置问题。比如忘了开cpp后端,service 侧就找不到生成的 C++ 头文件;或者stability没设成vintf,编译出来的接口不带稳定性标记,framework 拒绝加载。这些错误信息都不太直观,建议每次改完Android.bp先单独编译一下接口模块,确认没问题再往下走。
4. Service 侧实现:从继承接口到注册上线
4.1 继承 Bn 接口,实现业务逻辑
接口编译通过之后,会生成 C++ 的BnMySensor基类。Service 侧要做的就是继承这个基类,实现里面所有的纯虚函数。一个最简的实现大概长这样:
// hardware/interfaces/mysensor/default/MySensor.cpp #include <android/hardware/mysensor/BnMySensor.h> namespace android { namespace hardware { namespace mysensor { class MySensor : public BnMySensor { public: ndk::ScopedAStatus getValue(int32_t* _aidl_return) override { *_aidl_return = readHardwareValue(); return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus setThreshold(int32_t threshold) override { mThreshold = threshold; return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus getSensorName(std::string* _aidl_return) override { *_aidl_return = "MyCustomSensor"; return ndk::ScopedAStatus::ok(); } private: int32_t mThreshold = 0; int32_t readHardwareValue() { // 实际读取硬件的逻辑 return 42; } }; } // namespace mysensor } // namespace hardware } // namespace android注意返回类型是ndk::ScopedAStatus,不是普通的int或bool。这是 AIDL HAL 的错误处理机制,成功返回ndk::ScopedAStatus::ok(),失败返回对应的错误码。这个设计比 legacy HAL 返回负数错误码要清晰,但刚开始写的时候容易忘,编译报错会提示你返回类型不匹配。
4.2 main 函数里怎么把 service 注册上去
Service 实现好之后,需要一个main函数来启动它并注册到servicemanager。标准写法是这样:
// hardware/interfaces/mysensor/default/main.cpp #include <android/binder_manager.h> #include <android/binder_process.h> #include "MySensor.h" using android::hardware::mysensor::MySensor; int main() { ABinderProcess_setThreadPoolMaxThreadCount(0); std::shared_ptr<MySensor> sensor = ndk::SharedRefBase::make<MySensor>(); const std::string instance = std::string() + MySensor::descriptor + "/default"; binder_status_t status = AServiceManager_addService( sensor->asBinder().get(), instance.c_str()); if (status != STATUS_OK) { return -1; } ABinderProcess_joinThreadPool(); return 0; }这里有几个细节值得说。ABinderProcess_setThreadPoolMaxThreadCount(0)是告诉 Binder 线程池按需创建线程,对于 HAL service 来说通常够用。instance的命名规则是<接口名>/<实例名>,default是最常见的实例名,但如果你有多个同类设备,可以用sensor0、sensor1这种区分。
AServiceManager_addService返回STATUS_OK才算注册成功。如果返回其他值,通常是servicemanager没起来或者 SELinux 策略拦住了。我遇到过注册失败但没有任何日志的情况,后来发现是 SELinux 的allow规则没加,service 进程连servicemanager的 socket 都连不上。这个坑后面会专门讲。
4.3 Android.bp 里 service 模块的配置
Service 的Android.bp比接口的要复杂一些,需要链接 AIDL 生成的库、Binder 库、以及可能的硬件访问库。一个典型的配置:
cc_binary { name: "android.hardware.mysensor-service", vendor: true, relative_install_path: "hw", init_rc: ["mysensor-default.rc"], vintf_fragments: ["mysensor-default.xml"], srcs: [ "main.cpp", "MySensor.cpp", ], shared_libs: [ "libbase", "libbinder_ndk", "libcutils", "liblog", "android.hardware.mysensor-V1-ndk", ], static_libs: [ "libutils", ], cflags: [ "-Wall", "-Werror", ], }vendor: true表示编译到 vendor 分区,relative_install_path: "hw"让它装到/vendor/bin/hw/下面,这是 HAL service 的惯例路径。init_rc和vintf_fragments分别指向 init 启动脚本和 VINTF 清单片段,这两个文件是 service 能被系统识别和启动的关键。
shared_libs里的android.hardware.mysensor-V1-ndk就是前面aidl_interface编译出来的库,名字格式是<接口名>-V<版本>-ndk。如果这里名字写错了,链接阶段会报找不到符号,错误信息里会列出缺失的符号名,对着接口文件检查一下就能定位。
5. 让系统认识你:init、VINTF 与 SELinux 三件套
5.1 init.rc 脚本:service 怎么被拉起来
HAL service 不是自己启动的,而是由 init 进程根据.rc脚本拉起来的。一个标准的mysensor-default.rc长这样:
service vendor.mysensor-default /vendor/bin/hw/android.hardware.mysensor-service class hal user system group system capabilities SYS_NICE file /dev/mysensor_dev 0660 system systemclass hal表示这是 HAL 类服务,系统启动到 hal 阶段时会拉起它。user和group决定 service 进程的权限,通常用system就够了,除非你的硬件访问需要特殊权限。capabilities SYS_NICE是给进程加能力,不是所有 HAL 都需要,按实际情况加。
file那一行是给设备节点设置权限,如果你的 HAL 要访问/dev/下面的设备节点,必须在这里声明,否则 service 进程没有权限打开。我当年做传感器的时候就是忘了这一行,service 起来了但读不到数据,logcat 里只有Permission denied,查了半天才发现是设备节点权限没配。
5.2 VINTF 清单:framework 怎么找到你
VINTF 清单是 AIDL HAL 最容易被忽略、也最容易出错的部分。它告诉系统“这个 HAL 提供了什么接口、什么版本、什么实例”。一个典型的mysensor-default.xml:
<manifest version="1.0" type="device"> <hal format="aidl"> <name>android.hardware.mysensor</name> <version>1</version> <fqname>IMySensor/default</fqname> </hal> </manifest>format="aidl"是关键,如果是 HIDL 就写hidl。name要和接口包名一致,version要和aidl_interface里的版本对应,fqname是<接口名>/<实例名>,要和main.cpp里注册的 instance 完全一致。
这里任何一个字段写错,lshal里都看不到你的 service。我见过最常见的是fqname里的接口名忘了去掉I前缀,或者实例名写成了default但代码里注册的是sensor0。这种错误不会有明显报错,只能靠lshal和dumpsys去对比排查。
5.3 SELinux 策略:最隐蔽的拦路虎
SELinux 是 AIDL HAL 调试中最让人头疼的部分。即使前面所有配置都对了,SELinux 策略没加,service 照样起不来或者注册失败。需要加的策略通常包括三部分:
第一,给 service 进程定义类型。在vendor/file_contexts里加:
/vendor/bin/hw/android\.hardware\.mysensor-service u:object_r:hal_mysensor_default_exec:s0第二,定义 domain 和基本规则。在vendor/hal_mysensor_default.te里:
type hal_mysensor_default, domain; type hal_mysensor_default_exec, exec_type, vendor_file_type, file_type; init_daemon_domain(hal_mysensor_default) hal_server_domain(hal_mysensor_default, hal_mysensor)第三,允许 service 注册到 servicemanager:
allow hal_mysensor_default hwservicemanager_prop:file { read open getattr }; allow hal_mysensor_default servicemanager:binder { call transfer };这些规则看起来繁琐,但每一条都有明确用途。init_daemon_domain让 init 能启动这个 service,hal_server_domain把 service 和 HAL 类型关联起来,后面的allow规则分别允许读取属性和注册 Binder。
我踩过的最深的坑是:SELinux 拒绝日志默认不显示在 logcat 里,需要dmesg或者audit2allow才能看到。第一次遇到 service 静默失败的时候,我以为是代码问题,查了两个小时才发现是 SELinux 在拦。后来养成了习惯,service 起不来先看dmesg | grep avc,能省很多时间。
6. 编译、部署与验证的完整链路
6.1 单独编译接口和 service
整个模块写完之后,不要急着全系统编译,先单独编译接口和 service,能快速定位问题。接口模块:
m android.hardware.mysensor-V1-ndkService 模块:
m android.hardware.mysensor-service如果接口编译报aidl文件生成失败,重点检查.aidl文件的包名和路径是否匹配、Android.bp里的srcs路径是否正确。如果 service 编译报链接错误,重点检查shared_libs里的库名是否和接口模块名一致。
单独编译通过之后,再m全系统编译,把生成的.so、可执行文件、.rc、.xml都打包进镜像。这一步通常不会出问题,但如果前面有模块没被依赖到,可能会漏打包,所以全编之后最好确认一下/vendor/bin/hw/和/vendor/etc/vintf/manifest/下面有没有你的文件。
6.2 推送到设备并手动启动
调试阶段不建议每次都刷机,可以用adb push把文件推到设备上手动启动。步骤大概是:
adb root adb remount adb push android.hardware.mysensor-service /vendor/bin/hw/ adb push mysensor-default.rc /vendor/etc/init/ adb push mysensor-default.xml /vendor/etc/vintf/manifest/ adb shell chmod 755 /vendor/bin/hw/android.hardware.mysensor-service adb shell start vendor.mysensor-defaultadb remount需要设备支持,有些量产设备锁了 remount,那就只能刷机。手动启动之后,用adb shell ps -A | grep mysensor确认进程起来了,用adb shell lshal | grep mysensor确认 HAL 被系统识别了。
这里有个小技巧:如果start之后进程立刻退出,可以先adb shell logcat | grep mysensor看有没有崩溃日志,再看adb shell dmesg | grep avc看有没有 SELinux 拒绝。这两个地方能覆盖 90% 的启动失败原因。
6.3 用 lshal 和 dumpsys 验证接口可用
Service 起来之后,验证接口是否真的可用。lshal能看到所有已注册的 HAL:
adb shell lshal | grep mysensor正常输出应该包含接口名、版本、实例名、进程 PID 等信息。如果这里看不到,说明 VINTF 清单或者注册环节有问题。
进一步验证接口调用,可以写一个简单的测试客户端,或者用dumpsys看 framework 侧有没有成功连接。对于自定义 HAL,通常需要自己写测试代码,调用IServiceManager::getService拿到代理,然后调用接口方法,确认返回值正确。这一步能跑通,基本就说明整个链路没问题了。
7. 那些年我踩过的坑和排查思路
7.1 service 起来了但 lshal 看不到
这是最常见的问题,排查顺序我总结成了一张表:
| 排查点 | 检查方法 | 常见问题 |
|---|---|---|
| VINTF 清单 | 检查 xml 字段 | fqname 拼写错误 |
| 接口稳定性 | 检查 @VintfStability | 注解漏加 |
| 注册实例名 | 对比代码和 xml | 实例名不一致 |
| SELinux | dmesg 看 avc | 缺 allow 规则 |
| 清单路径 | 确认 xml 位置 | 放错目录 |
按这个顺序查,基本能覆盖所有情况。我遇到最多的是 fqname 拼写错误和@VintfStability漏加,这两个都是低级错误但很容易犯。
7.2 Binder 调用返回异常怎么定位
接口能调用但返回异常,通常是业务逻辑或者权限问题。先看ScopedAStatus返回的具体错误码,EX_ILLEGAL_ARGUMENT一般是参数问题,EX_SECURITY是权限问题,EX_UNSUPPORTED_OPERATION是接口没实现。根据错误码去对应的代码路径排查,比盲目看日志高效得多。
如果是跨进程传输复杂结构体出错,重点检查 parcelable 的字段顺序和类型是否和.aidl声明一致。AIDL 的序列化是严格按照声明顺序来的,字段顺序错了不会报编译错误,但运行时数据会错位,这种问题最难查。
7.3 接口版本升级时的兼容性处理
接口上线之后难免要改,AIDL HAL 的版本管理比想象中严格。加新方法要升版本号,改现有方法签名基本等于不兼容,只能新开接口。aidl_interface的versions字段就是干这个的,第一次是["1"],加方法之后变成["1", "2"],framework 侧根据版本号选择调用哪个。
我建议接口设计之初就多留一点扩展空间,比如预留一些通用参数,避免频繁升版本。VINTF 兼容性检查在编译期就会做,不兼容的改动直接编译不过,这一点比 legacy HAL 要安全,但也意味着改接口的成本更高。
8. 从能跑到好用:几个提升开发效率的习惯
第一个习惯是给每个 HAL 写一个独立的测试客户端。不要依赖 framework 去验证,自己写个小的 C++ 程序,直接getService然后调用接口,能快速确认 service 本身是否正常。这样排查问题时能把 framework 侧的因素排除掉,定位范围小很多。
第二个习惯是善用lshal --debug和dumpsys。lshal能看到 HAL 的注册状态和接口列表,dumpsys能看到 framework 侧的使用情况。两者结合,基本能判断问题出在 service 侧还是 framework 侧。
第三个习惯是 SELinux 策略提前加。不要等 service 起不来才去补,写 service 的时候就顺手把.te和file_contexts加上,能省很多来回折腾的时间。SELinux 拒绝日志默认不显示,养成看dmesg的习惯很重要。
最后说一个我自己的体会:AIDL HAL 的学习曲线确实比 legacy HAL 陡,但一旦跑通一次完整流程,后面再做新的 HAL 就是复制粘贴改改名字的事。真正难的是第一次,把接口定义、service 实现、init、VINTF、SELinux 这五块都走一遍,后面就顺了。我当年卡了整整一周,现在回头看,大部分时间都花在那些没有明确报错的配置问题上。希望这篇内容能帮你把这一周压缩到一两天。