ARTICLE DETAIL

资讯详情

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

AIDL HAL升级实战:从HIDL迁移到Android新接口体系的完整指南

AIDL HAL升级实战:从HIDL迁移到Android新接口体系的完整指南 最近一直在折腾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 11Google开始推动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版本控制与VintfStabilityAIDL 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.sensor1.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 version1.0 typedevice hal formataidl namevendor.acme.hardware.sensor/name version1/version interface nameISensor/name instancedefault/instance /interface /hal /manifest注意format必须写成aidl不能沿用HIDL时代的formathidl。而且一个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线程池死锁。我习惯把所有回调方法都声明成onewayVintfStability 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的声明且formataidl。再确认声明中的name、version、instance与AIDL包名、服务注册名完全一致。使用vintf命令行工具或者开机log里的VINTF错误判断是否校验失败典型报错是Incompatible HAL declaration。检查/dev/vndbinder是否正常工作有时vendor binder异常会导致服务注册不到binder域。有一次我们升级了一个HALVINTF声明都正确服务也在正常跑但system server那边就是发现不了服务。最后发现是old版本的HIDL接口还留在manifest里和新的AIDL接口产生了重名冲突。把旧条目删掉后问题立刻消失。5.3 TransactionTooLarge和binder回调问题AIDL Binder的TransactionTooLarge是一个经典问题。AIDL的Binder事务默认有1MB的缓冲限制但实际可用额度会因为binder内核驱动的共享内存设置而减小。很多HAL在升级之前用HIDL的共享内存机制数据传递不需要走Binder大事务迁移到AIDL后如果没改成SharedMemory异常就会集中爆发。事务过大通常表现为调用时系统日志打印TransactionTooLargeException或者客户端一直拿不到返回值。解决办法是把大块数据改到SharedMemoryBinder只传递一个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方向优化一下到时候再继续写笔记分享。
返回列表