拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android AIDL HAL 从零到一:接口定义、Service 注册与 VINTF 配置实战

Android AIDL HAL 从零到一:接口定义、Service 注册与 VINTF 配置实战

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,答案很明确:学 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 system

class 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-ndk

Service 模块:

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-default

adb 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实例名不一致
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 这五块都走一遍,后面就顺了。我当年卡了整整一周,现在回头看,大部分时间都花在那些没有明确报错的配置问题上。希望这篇内容能帮你把这一周压缩到一两天。

返回列表