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 HALHIDL HALAIDL HAL通信方式同进程函数调用Binder IPCBinder IPC接口定义头文件 结构体.hal文件.aidl文件进程隔离无有有独立升级困难支持支持Android 版本8.0 前主流8.0-11 主流11 后推荐调试工具logcatlshallshal aidl 工具学习曲线低中中高从表里能看出来AIDL HAL 并不是凭空冒出来的它是 HIDL 的继任者。Google 在 Android 11 之后把 HIDL 标记为 deprecated新 HAL 一律用 AIDL。所以如果你现在还在纠结学 HIDL 还是 AIDL答案很明确学 AIDLHIDL 只需要能看懂老代码就行。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(typeint)注解不然编译不过。我建议的做法是能用基本类型就用基本类型复杂数据尽量拆成多个简单字段。这样虽然接口看起来啰嗦一点但稳定性和可调试性好很多。当年我为了图省事在一个接口里塞了个嵌套结构体结果后面改字段的时候发现 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对应前面说的VintfStabilitybackend里按需开启 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_ptrMySensor sensor ndk::SharedRefBase::makeMySensor(); 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.xmlmanifest version1.0 typedevice hal formataidl nameandroid.hardware.mysensor/name version1/version fqnameIMySensor/default/fqname /hal /manifestformataidl是关键如果是 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 注册到 servicemanagerallow hal_mysensor_default hwservicemanager_prop:file { read open getattr }; allow hal_mysensor_default servicemanager:binder { call transfer };这些规则看起来繁琐但每一条都有明确用途。init_daemon_domain让 init 能启动这个 servicehal_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能看到所有已注册的 HALadb shell lshal | grep mysensor正常输出应该包含接口名、版本、实例名、进程 PID 等信息。如果这里看不到说明 VINTF 清单或者注册环节有问题。进一步验证接口调用可以写一个简单的测试客户端或者用dumpsys看 framework 侧有没有成功连接。对于自定义 HAL通常需要自己写测试代码调用IServiceManager::getService拿到代理然后调用接口方法确认返回值正确。这一步能跑通基本就说明整个链路没问题了。7. 那些年我踩过的坑和排查思路7.1 service 起来了但 lshal 看不到这是最常见的问题排查顺序我总结成了一张表排查点检查方法常见问题VINTF 清单检查 xml 字段fqname 拼写错误接口稳定性检查 VintfStability注解漏加注册实例名对比代码和 xml实例名不一致SELinuxdmesg 看 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 这五块都走一遍后面就顺了。我当年卡了整整一周现在回头看大部分时间都花在那些没有明确报错的配置问题上。希望这篇内容能帮你把这一周压缩到一两天。