最近一直在折腾AIDL HAL的升级,说白了就是把设备端HAL接口从HIDL这套体系迁到AIDL这套,同时还得保证老客户端不因为接口改动而崩溃。这篇笔记继续记录我在实际项目里踩过的一些坑和最终理出来的完整流程,包括接口怎么定义、稳定性怎么控制、VINTF怎么声明、SELinux怎么调,以及那些最容易被忽略的编译和运行期问题。如果你刚好负责把旧硬件接口现代化,或者准备在Android 14/15上新增vendor接口,这篇应该能帮你省不少时间。
1. 为什么要动AIDL HAL:升级的背景和真正原因
1.1 从HIDL到AIDL:不只是换了个接口语言
很多人一开始以为AIDL HAL就是把原来的.hal文件翻译成.aidl文件,改一下包名就完事。实际上这个动作背后的逻辑远不止语言层面的替换。HIDL是Android 8引入的,当时为了把system和vendor分区彻底解耦,专门搞了一套HAL接口描述语言,并通过独立的hwbinder驱动进行跨进程通讯。HIDL的设计目标很明确,就是让vendor实现可以稳定存在,不受上层框架频繁改动的影响。
但它的问题也很明显:系统里从此有了两套Binder,一套是传统Binder,另一套是hwbinder。JNI层、Java层、Native层各搞一套逻辑,工具链维护成本非常高。到了Android 11,Google开始推动AIDL HAL,把HAL接口统一到标准Binder上,让一套机制走天下。AIDL本身就是Android已经很成熟的跨进程接口工具,Java、C++、Rust都能直接生成代码,语言表达上比HIDL更接近普通Android开发者的直觉。
所以HIDL转AIDL本质上是帮系统做减法,减少双Binder维护,减少工具链复杂度,也让新功能的开发效率更高。到了Android 13之后,新设备上新增的HAL接口基本默认都要用AIDL,老设备上的HIDL接口则继续并存一段时间。所以我们现在做AIDL HAL升级,不是赶时髦,是在给以后的基础设施铺路。
1.2 接口升级的本质:稳定性、兼容性和版本演进
我在实际升级过程中发现,最容易出问题的反而不是代码本身,而是“接口稳定性”这个概念。HIDL时代,接口版本用@1.0、@1.1这种后缀描述,开发者会自然而然地知道一个HAL可能有多个版本。而AIDL HAL的版本管理相对内敛,它依赖@VintfStability注解、VINTF清单和冻结API的快照文件来控制接口变化。升级时如果不先把这套稳定性机制搞清楚,很可能一改接口,编译或者运行期就会给你来一个措手不及。
AIDL接口一旦投入生产,就相当于对外发布了协议,不能随便改已有方法的签名。你想给某个方法加参数,或者改返回值,这在旧接口上直接改就是破坏兼容性。正确的做法是新增一个接口版本,老版本代码继续保留,新客户端通过新版接口访问,老客户端还能继续使用旧版接口。AIDL HAL的升级很多时候并不是“替换”而是“叠加”,这个思路要先转过来。
1.3 升级前必须考虑的边界
另外,升级前还要想清楚边界问题。一个HAL接口往往会影响到vendor进程、system服务、框架Java代码、甚至vendor分区的init脚本。升级规模小的可能就一个服务进程,规模大的会牵扯到一堆依赖库和SELinux策略。我建议先把调用链画出来,明确哪些模块是必须一起改的,哪些可以保持old接口不动,哪些只做适配层。不然很容易出现一个接口升了,结果某个老进程还在用HIDL的FQName,系统起来之后找不到服务,开机动画半天加载不出来。
还要考虑分区的兼容性。如果设备运行的是Android 12,而vendor镜像还保留着Android 11的旧实现,那么AIDL HAL的新接口可能无法在旧vendor上工作。这种场景下不能盲目升级,需要先确认系统镜像和vendor镜像之间的版本匹配关系。实际操作中我习惯先看/vendor/etc/vintf/manifest.xml里已经声明了哪些HAL,再决定哪些接口可以一起升。
2. 升级前需要理清的几个核心概念
2.1 AIDL版本控制与@VintfStability
AIDL HAL和普通App内的AIDL最大的区别就在于对稳定性的要求。普通AIDL接口可以随便演进,同一进程内客户端和服务端一起更新就行。HAL接口不行,system和vendor可能是不同部门、不同发布周期,甚至不同厂商维护,接口必须在一开始就定好规则。
规则就是@VintfStability注解。在.aidl文件顶部给接口加上这个注解,它就会被标记为“可用于VINTF清单的稳定接口”。构建系统在生成Binder代码的时候,会使用一段可以让跨进程调用保持稳定的序列化逻辑。没有这个注解的AIDL接口,即使你强行注册到VINTF里,也会在运行时被系统拒绝。
实际开发中我会在所有的HAL接口文件开头加上:
@VintfStability interface ISensor { void getSensorList(); ... }同时构建模块里必须显式声明stability: "vintf"。这两个条件缺一不可,我遇到过忘记加注解导致运行时找不到服务的例子,排查过程非常痛苦。另外,@VintfStability只对接口、方法参数、返回值中涉及的类型有约束,不能在接口里随便使用普通App AIDL才允许的动态代理类型。
2.2 freeze机制和current.txt的作用
AIDL HAL编译时会生成一个API快照,也就是aidl_api/<interface>/current.txt文件。这个文件就是接口稳定性的“存档点”。一旦接口通过审查合入主干,并执行了freeze操作,之后你再修改同名接口的现有方法,构建系统会直接报错,提醒你破坏了接口兼容性。
这个机制有点像拍完照片之后,照片就不能再ps到面目全非,否则就认不出是同一个接口了。更新版本时,你需要生成新的版本目录,比如aidl_api/ISensor/V1/current.txt、aidl_api/ISensor/V2/current.txt。每次给接口添加新版本,都要重新执行freeze,把这个新版本的API快照保存下来。
我在项目里遇到过一个问题,就是本地改完AIDL文件,编译报错说current.txt不匹配。后来发现是因为我直接动了一个已经冻结的接口文件,而不是新增版本。正确做法是保留旧版本接口不变,新建一个新版本的AIDL接口文件,或者在同一个aidl_interface模块里通过versions字段管理多个版本。这个规则很死,但在多人协作时非常有用,能挡住不少无意的接口变更。
2.3 工具链:hidl2aidl与遗留代码的取舍
AOSP提供了一个hidl2aidl工具,可以辅助把HIDL的.hal文件转换成.aidl文件。但我的经验是,这个工具适合批量处理机械性的类型转换,比如一些枚举、结构体的改名和跨包调用,它远没有到“一键迁移”的程度。HIDL里有大量面向对象的包结构、泛型、回调接口,转换成AIDL后基本都是扁平化的,工具生成的骨架代码只能保证能编译,逻辑上完全不能保证正确。
我建议先把工具当成参考,不要当成自动翻译机。真正有价值的反而是手把手地梳理旧接口的语义。比如HIDL里的IMemory、IBase这种特殊接口,转换成AIDL之后往往要换成SharedMemory或者直接去掉。如果你的HAL里用了FMQ(快速消息队列)作为数据传输通道,那升级到AIDL后就要用AIDL自带的消息队列工具来替代,这个过程没法自动完成,只能靠人肉迁移。
3. 实操:从HIDL接口迁移到AIDL HAL的完整流程
3.1 生成初始化AIDL接口文件
我假设现在手上有一个老的HIDL接口vendor.acme.hardware.sensor@1.0::ISensor,要迁移到AIDL包vendor.acme.hardware.sensor.ISensor。第一步不是在源码里新建空文件,而是先列出旧接口的所有方法、枚举、结构体、回调接口,然后逐一映射成AIDL定义。
一个典型的迁移结果长这样:
// vendor/acme/hardware/sensor/aidl/ISensor.aidl package vendor.acme.hardware.sensor; @VintfStability interface ISensor { void setCallback(in ISensorCallback callback); SensorStatus getStatus(); SensorData readData(); }HIDL里的方法参数默认传递方向在AIDL里必须明确标出in、out、inout。很多人第一次迁移时容易忽略这个,结果发现返回数据根本没带回来。就是因为AIDL默认如果没标方向,某些情况下编译器会按in处理,out参数根本不会被回传。
如果旧接口里有一些类似HIDL的generate()回调模式,转成AIDL时尽量保持回调用oneway关键字,避免客户端和服务端在binder线程池里互相等待。
3.2 搭建aidl_interface构建模块
AIDL HAL的构建模块和普通App AIDL不一样,不是用android_library或者cc_aidl,而是用aidl_interface。这个模块会帮你处理版本管理、接口稳定性、VINTF兼容性检查等一系列内容。
我的典型Android.bp配置是:
aidl_interface { name: "vendor.acme.hardware.sensor", srcs: [ "ISensor.aidl", "ISensorCallback.aidl", "SensorStatus.aidl", ], stability: "vintf", vendor: true, }这里有几个点要注意。vendor: true表示这是一个真正给vendor进程用的HAL,而不是系统私有接口。stability: "vintf"配合接口文件里的@VintfStability,缺一个都会导致运行时异常。
另外,如果你的接口里有Parcelable结构体,需要单独建一个aidl_interface模块来定义这些类型,然后在主接口模块中用import引入。不同包之间的AIDL类型是不能直接在同一个构建模块里揉在一起的。这个规则和HIDL里的包管理有点类似,但也有区别。
3.3 更新VINTF manifest与SELinux策略
接口编译通过之后,还需要让系统知道这个HAL的存在。在AIDL HAL架构下,所有需要暴露给system分区的vendor接口,都必须声明在VINTF清单里。常见的/vendor/etc/vintf/manifest.xml片段长这样:
<manifest version="1.0" type="device"> <hal format="aidl"> <name>vendor.acme.hardware.sensor</name> <version>1</version> <interface> <name>ISensor</name> <instance>default</instance> </interface> </hal> </manifest>注意format必须写成aidl,不能沿用HIDL时代的format="hidl"。而且一个<hal>节点内可以声明多个接口和实例,同一个HAL包的多版本也可以一起声明,但建议在升级初期保持minimal,只声明实际存在的实例,减少VINTF校验失败的干扰。
SELinux策略也是重头戏。新增AIDL HAL服务时,需要给binder服务名分配一个SELinux context。常见的service_contexts文件里会加一行:
vendor.acme.hardware.sensor.ISensor/default u:object_r:hal_sensor_service:s0然后还要在对应的.te文件里允许system_server或其他客户端调用这个服务:
binder_call(system_server, hal_sensor_service);如果漏掉SELinux配置,服务端进程是能正常启动的,但客户端一调用就会在log里看到permission denied。这类问题我后面会专门讲排查方法。
3.4 实现服务端并注册
代码生成和VINTF声明都完成后,就轮到写真正的实现。AIDL HAL服务端是一个独立的Native进程或者作为现有vendor进程里的一个Binder服务注册。注册时一般用defaultServiceManager()->addService()或者AServiceManager_addService。
C++实现大概会分成两部分:一个继承生成代码里的BpInterface和BnInterface实现类,另一个是main函数里创建实现实例并注册服务。这里我踩过一个坑,就是注册的服务名必须和VINTF清单里的instance完全一致,大小写都要一致。之前把default写成Default,导致客户端能拿到Binder对象但VINTF校验不过,开机log一直报找不到匹配的实例。
另外,实现所有接口方法时,要注意binder事务的方向。AIDL接口里如果定义了oneway方法,服务端实现中不能在里面再调用会阻塞的Binder方法,否则会让整个binder线程池卡死,表现出来就是接口偶发超时。
3.5 客户端改造与联调验证
客户端改造相对简单,在Android.bp里关联了aidl_interface模块后,可以使用生成的代理类。Java侧通过ISensor.Stub.asInterface(binder)拿到接口,Native侧类似。这一步最需要验证的是“客户端是不是还按老思路在找服务”。
以前HIDL会通过ISensor::getService()获取服务,切换AIDL后,要改成从AServiceManager或ServiceManager里按VINTF名查找。如果你改动了一部分代码,但还有一行老代码在按HIDL名字取服务,编译能过,运行起来却永远拿不到。
联调的时候我会先做一个小工具,直接通过service list | grep sensor查看服务是否注册成功,再通过一个测试App或测试程序调用最基础的方法确认通路。顺序一定要从底层往上层走,先确认Binder注册正常,再确认VINTF可见,再确认SELinux放行,最后才去调具体业务方法,这样能缩短排查时间。
4. AIDL HAL接口自身升级时的兼容性细节
4.1 新增方法时怎么保持客户端不崩
从旧AIDL接口升级到新AIDL接口时,最常见的方式是在新版本接口里增加新方法,旧版本接口保持原样。但这里有个容易被忽略的问题:如果客户端拿到的是旧接口代理对象,然后跨进程调用一个服务端其实已经不存在的方法,会发生什么?答案不是崩溃,而是得到一个UNKNOWN_TRANSACTION或者类似异常。如果异常处理不当,上层服务就会把这个错误当成业务失败,进而触发错误逻辑。
我的做法是,在服务端新版本实现里,对旧接口的方法保持兼容实现,至少返回一个明确错误码,不要直接让远程调用落到未知事务上。同时,客户端升级后要主动判断拿到的接口版本,再决定调用哪些方法。AIDL不像HIDL那样强制接口带版本号,所以需要开发者在接口定义里自己维护一个getVersion方法,或者通过@VintfStability接口的版本信息判断。
4.2 数据结构的演进:parcelable的javaOnly与稳定性
接口里不可避免要用复杂数据结构。HIDL里的struct、enum、union转到AIDL时,一般会变成Parcelable。普通的Parcelable类可以用Java或C++定义,但稳定AIDL接口里的Parcelable定义必须严格遵循规则,不能使用平台里和供应商实现绑定过深的类型。
一个比较隐蔽的坑是@JavaOnlyStableParcelable。这个注解表示某个Parcelable只允许在Java层使用,不能用于Native侧的稳定AIDL接口。很多从HIDL迁移过来的结构体习惯性用Java类定义,结果放到稳定AIDL接口里就编译不过。原因不是代码有问题,而是因为这种类型没有对应的C++稳定实现。
升级数据结构时,尽量把每个字段的传输方式想清楚。字段适合放在Parcel里直接传,大块数据不要放进去,应该用SharedMemory或文件描述符来传。之前有个兄弟把一张几MB的图片直接塞进AIDL方法参数里,结果调用一次就爆一次TransactionTooLarge,后来改成传共享内存的fd才稳定下来。
4.3 大坑:回调接口和异步消息的处理
很多HAL都依赖回调接口,比如传感器数据上报、状态变化通知。HIDL时代的回调接口有一套自己的生命周期管理方式,转成AIDL后,这些回调接口也要标记@VintfStability。如果回调接口本身不稳定,服务端在跨进程注册回调时就会被VINTF机制挡住。
AIDL回调的另一个大坑是线程模型。服务端收到客户端注册的callback代理对象后,如果在一个关键路径上同步调用这个callback,而callback实现的onReceive里又反过来调服务端的方法,这就有概率造成binder线程池死锁。我习惯把所有回调方法都声明成oneway:
@VintfStability interface ISensorCallback { oneway void onDataReceived(in SensorData data); oneway void onStatusChanged(in SensorStatus status); }这个改动很小,但对稳定性提升非常明显。oneway的Binder调用不需要等待对端返回,即使对端处理慢,也不会阻塞服务端自己的线程。
同时要注意,客户端持有了服务端传出来的callback,就存在跨进程对象生命周期问题。AIDL的Binder代理对象如果被客户端进程持有多余的引用,可能导致服务端无法释放资源。我建议在服务端为每个客户端建立一个session,注销时显式清掉session里的callback对象,而不是依赖系统自动回收。
5. 常见问题与排错实录
5.1 aidl文件生成失败:常见的构建时错误
升级过程中遇到最多的问题肯定是编译报错。我遇到过好几种“aidl文件生成失败”的情况,这里列几个最容易踩的:
- 接口文件里写了
@VintfStability,但Android.bp对应模块没有stability: "vintf",构建系统直接拒绝生成。 - 一个包里的多个AIDL文件互相引用,但漏掉其中一个没有放进
srcs,编译时提示找不到符号。 - 使用Parcelable时,没有在独立模块里声明这个Parcelable,编译器不知道去哪找类型定义。
- 修改了已冻结的接口,导致
current.txt校验失败。
第一个坑和第四个坑尤其隐蔽。第一次遇到时,我差点以为是Android.bp写错了,后来仔细看报错日志才明白是接口稳定性校验在拦截。养成新版本接口就新建版本目录的习惯,能省下很多无意义的排查时间。
5.2 VINTF校验失败和服务找不到
运行期最常见的现象是客户端获取服务时拿到的binder为null,或者ISensor::getService()直接返回失败。遇到这类问题,第一反应不是怀疑代码,而是去看VINTF校验的日志。
我一般用这几个步骤排查:
- 先确认
/vendor/etc/vintf/manifest.xml里有对应HAL的声明,且format="aidl"。 - 再确认声明中的
name、version、instance与AIDL包名、服务注册名完全一致。 - 使用
vintf命令行工具或者开机log里的VINTF错误判断是否校验失败,典型报错是Incompatible HAL declaration。 - 检查
/dev/vndbinder是否正常工作,有时vendor binder异常会导致服务注册不到binder域。
有一次我们升级了一个HAL,VINTF声明都正确,服务也在正常跑,但system server那边就是发现不了服务。最后发现是old版本的HIDL接口还留在manifest里,和新的AIDL接口产生了重名冲突。把旧条目删掉后,问题立刻消失。
5.3 TransactionTooLarge和binder回调问题
AIDL Binder的TransactionTooLarge是一个经典问题。AIDL的Binder事务默认有1MB的缓冲限制,但实际可用额度会因为binder内核驱动的共享内存设置而减小。很多HAL在升级之前用HIDL的共享内存机制,数据传递不需要走Binder大事务;迁移到AIDL后如果没改成SharedMemory,异常就会集中爆发。
事务过大通常表现为调用时系统日志打印TransactionTooLargeException,或者客户端一直拿不到返回值。解决办法是把大块数据改到SharedMemory,Binder只传递一个SharedMemory对象或fd。如果是重复上报的场景,用AIDL的FMQ会更合适。
另外一个隐蔽问题是Binder回调时如果client进程死了,服务端再调用callback会得到DeadObjectException。这个异常一定要在服务端捕获,并且及时清理客户端资源,否则服务端会积累一堆幽灵回调对象,导致内存缓慢上涨。
5.4 SELinux denied排查
SELinux虽然烦人,但逻辑很简单,几乎所有权限问题都会在dmesg或logcat里留下avc denied的痕迹。排查命令我常用:
adb root adb shell dmesg | grep avc看到类似avc: denied { call } for scontext=... tcontext=...的日志后,只需要把缺失的规则补进对应的.te文件。
在AIDL HAL升级中,新增的binder服务名和旧服务的SELinux context如果一样,可能没问题;如果改名了,那就必须重新分配context并更新所有调用方的权限。不要想当然沿用旧规则,否则最常见的现象是服务能启动,但客户端一调用就被denied。
6. 升级这件事,我的几点体会
做完一整轮AIDL HAL升级后,我最大的体会是:这个工作真正难的不是写接口,而是管理好兼容性预期。接口一旦冻结,就要时刻记住自己不是一个人在改代码,还有很多下游模块会依赖这些老方法。升级时最怕的不是报错,而是“看起来一切正常,但某些设备上某些老进程突然拿不到服务”。
我习惯在动手之前先把旧的HIDL接口、VINTF条目、SELinux策略、客户端调用点全部列一个清单,再按依赖关系排优先级,分批切换。每完成一步都要验证一步,不要试图一次性把所有接口全部翻新,那只会让问题叠加到无法定位。
整个过程下来,我觉得最有用的一个技巧是,在接口升级期间保留一套旧HIDL服务的兜底实现,等新的AIDL服务在新设备上稳定运行几个版本后,再彻底移除旧实现。这样既能让新接口快速落地,又能保证老设备上的功能不受影响。就算出现极端兼容性问题,回滚也只是切换回旧服务一条命令的事情。
AIDL HAL的升级在未来几年会是Android设备开发的一个主流动作,我建议早做规划、小步快跑,别等服务中断了才想起来改接口。我这边后续还要把数据通道再往FMQ方向优化一下,到时候再继续写笔记分享。