ARTICLE DETAIL

资讯详情

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

从零构建AIDL HAL服务:Android硬件抽象层开发实战指南

从零构建AIDL HAL服务:Android硬件抽象层开发实战指南 1. 为什么还要折腾AIDL HAL如果你最近两三年才开始接触Android系统开发可能会觉得HAL这东西有点分裂。一边是Android 8.0引入的HIDL一边是Android 11之后Google力推的AIDL HAL网上搜一圈资料要么是HIDL时代的遗留要么是AOSP源码里那种“看得懂每个字但连起来不知道在干嘛”的注释。更别提那些热搜词里冒出来的“aidl文件生成失败”“android aidl所有的考点”一看就是被编译报错折磨过的兄弟搜出来的。我写这篇东西的起因很简单手上有个定制硬件的项目需要从零搭一个AIDL HAL服务让上层App能通过标准接口访问我们自己的传感器模块。翻了一圈官方文档发现它默认你已经懂了HIDL、懂了binder、懂了selinux、懂了init.rc然后直接甩给你一堆.aidl文件和Android.bp。对于第一次上手的人来说这个门槛其实挺陡的。所以这篇博文的目标很明确把“从零到一构建一个AIDL HAL服务”这件事拆成一条能走通的路。我会假设你手上有AOSP源码树知道怎么编译但对AIDL HAL的具体落地流程还不熟。如果你是完全没碰过Android系统开发的小白建议先补一下binder通信和Android构建系统的基础不然中间某些步骤会卡住。先把这个东西是什么说清楚。AIDL HAL全称是Android Interface Definition Language Hardware Abstraction Layer。拆开看AIDL是接口定义语言用来描述跨进程通信的接口HAL是硬件抽象层是Android框架和硬件驱动之间的一层隔离。合起来就是用AIDL定义的、运行在独立进程里的、通过binder和上层通信的硬件抽象服务。它能做什么简单说你有一个硬件模块比如一个自定义的传感器、一个特殊的LED控制器、或者一个工业设备上的串口模块你想让Android的上层应用或者框架服务能调用它但又不想把驱动代码直接塞进system_server或者某个系统App里。这时候AIDL HAL就是标准做法。适合谁看适合已经能编译AOSP、知道vendor分区和system分区区别、写过至少一个HIDL或AIDL服务的Android系统开发工程师。如果你正在被“aidl文件生成失败”折磨或者想搞清楚“hal文件”到底该怎么组织这篇应该能帮你省几天时间。2. 整体设计AIDL HAL的骨架长什么样2.1 从HIDL到AIDL为什么换Google在Android 11开始推AIDL HAL原因不复杂。HIDL是专门为HAL设计的有自己的一套语法和工具链和Android其他部分的AIDL是两套东西。维护两套IPC机制成本高而且HIDL的某些设计在稳定性、版本管理上不如AIDL灵活。AIDL经过多年演进已经支持稳定版本、支持多种语言后端、支持更细粒度的接口控制。所以Google的决定是统一到AIDLHAL也用AIDL写。对我们开发者来说最直接的变化是你不再需要写.hal文件了改成写.aidl文件。工具链从hidl-gen变成了aidl_api和aidl工具。编译规则从Android.bp里的hidl_interface变成了aidl_interface。init.rc里的服务启动方式也有变化。但核心思想没变HAL服务跑在独立进程通过binder暴露接口上层通过servicemanager或者hwservicemanager找到它。AIDL HAL用的是标准的servicemanager不是HIDL那个hwservicemanager。2.2 一个AIDL HAL服务的完整组成我画不了图但可以文字描述。一个完整的AIDL HAL服务从下到上包括这几层内核驱动层你的硬件驱动可能是内核模块也可能是用户态驱动。这一层和AIDL HAL没有直接关系HAL服务通过open/ioctl/read/write或者sysfs节点访问它。HAL服务实现层一个C或Java/Rust写的可执行文件里面实现了AIDL接口定义的所有方法。这个可执行文件通常放在/vendor/bin/hw/目录下。AIDL接口定义层.aidl文件定义了接口方法、数据类型、回调等。这些文件放在源码树的某个interface目录下编译后会生成对应的C/Java/Rust代码。VINTF声明层一个XML文件声明你的HAL服务提供了哪个接口的哪个版本。这个文件放在/vendor/etc/vintf/manifest.xml或者单独的fragment里。init启动脚本在init.rc或者vendor的init脚本里注册服务让init在开机时启动它。SELinux策略定义HAL服务进程的domain、允许它访问的设备节点、允许它和哪些进程通信。这六层缺一不可。少一个要么编译不过要么开机起不来要么起来了上层找不到。2.3 目录结构怎么规划在AOSP源码树里AIDL HAL通常放在vendor/或者hardware/目录下。我习惯这样组织vendor/yourcompany/hardware/yourdevice/ ├── aidl/ │ └── com/yourcompany/hardware/yourdevice/ │ ├── IYourDevice.aidl │ ├── IYourDeviceCallback.aidl │ └── types.aidl ├── default/ │ ├── YourDevice.cpp │ ├── YourDevice.h │ ├── service.cpp │ └── Android.bp ├── aidl_api/ │ └── com.yourcompany.hardware.yourdevice/ │ └── 1/ │ └── com/yourcompany/hardware/yourdevice/ │ ├── IYourDevice.aidl │ └── ... └── vintf/ └── yourdevice.xml这个结构不是强制的但符合AOSP的惯例。aidl/放接口定义default/放默认实现aidl_api/放冻结的接口版本vintf/放VINTF声明。编译的时候aidl_interface模块会从aidl/目录读取接口生成代码cc_binary模块会编译default/下的实现链接生成的代码。注意aidl_api目录是给接口冻结用的。当你第一次发布接口后需要把当前版本的.aidl文件复制到aidl_api/interface_name/ /下面并生成一个hash文件。这样后续修改接口时编译系统会检查兼容性。很多“aidl文件生成失败”的问题根源就是aidl_api目录没配好或者hash对不上。3. 核心细节AIDL接口定义与编译3.1 .aidl文件怎么写AIDL的语法和Java接口很像但有一些HAL特有的约束。先看一个最简单的例子// IYourDevice.aidl package com.yourcompany.hardware.yourdevice; import com.yourcompany.hardware.yourdevice.IYourDeviceCallback; VintfStability interface IYourDevice { int getVersion(); boolean init(); int readData(out byte[] data); void registerCallback(IYourDeviceCallback callback); void unregisterCallback(IYourDeviceCallback callback); }几个关键点VintfStability注解这是AIDL HAL必须加的。它告诉编译系统这个接口是稳定的会参与VINTF兼容性检查。不加的话编译出来的接口不能用于HAL。package名必须是com.android.hardware.xxx或者com.yourcompany.hardware.xxx这种格式。HAL接口的package名有命名规范不能随便起。out参数AIDL支持in/out/inout参数。对于HAL来说out参数用来返回数据比如读取传感器数据。回调接口如果HAL需要主动通知上层需要定义一个回调接口通常是IYourDeviceCallback。再看回调接口// IYourDeviceCallback.aidl package com.yourcompany.hardware.yourdevice; VintfStability interface IYourDeviceCallback { void onDataAvailable(in byte[] data); void onError(int errorCode); }回调接口也需要VintfStability。注意回调是单向的上层注册后HAL服务在数据到来时调用onDataAvailable。3.2 Android.bp怎么配接口定义好了接下来是编译规则。在aidl/目录下放一个Android.bpaidl_interface { name: com.yourcompany.hardware.yourdevice, vendor_available: true, srcs: [com/yourcompany/hardware/yourdevice/*.aidl], stability: vintf, backend: { cpp: { enabled: true, }, java: { enabled: false, }, ndk: { enabled: true, }, }, versions: [1], }这里有几个参数需要解释vendor_available: true表示这个接口可以被vendor分区的模块使用。HAL服务通常在vendor分区所以必须开。stability: vintf表示接口是VINTF稳定的。这个和.aidl文件里的VintfStability注解是对应的。backend指定生成哪些语言的代码。HAL服务用C写所以cpp和ndk都开。Java后端一般关掉除非你要用Java写HAL。versions: [1]接口版本。第一次写就写1。后续如果要加新方法需要升版本并且把旧版本的.aidl冻结到aidl_api目录。实操心得如果你在编译时遇到“aidl文件生成失败”或者“cannot find interface”之类的错误先检查三件事package名和目录结构是否匹配、VintfStability是否加了、aidl_interface的name是否和package名一致。这三个地方最容易出错。3.3 接口冻结与版本管理AIDL HAL的版本管理比HIDL严格。一旦你的接口被某个设备使用就不能随便改。要改的话必须走冻结流程。冻结流程是这样的假设当前接口版本是1你要加一个新方法。首先把当前aidl/目录下的所有.aidl文件复制到aidl_api/com.yourcompany.hardware.yourdevice/1/下面保持目录结构一致。然后在aidl_api/com.yourcompany.hardware.yourdevice/1/下面会生成一个hash文件记录当前接口的哈希值。接着修改aidl/目录下的.aidl文件加上新方法并把Android.bp里的versions改成[1, 2]。编译时aidl工具会检查版本1的接口是否和冻结的hash一致版本2的接口是否是版本1的超集。这个过程听起来简单但实际操作时经常出问题。最常见的是改了aidl/下的文件忘了更新aidl_api/下的冻结文件导致编译报“interface changed but not frozen”。解决办法就是严格按照流程走每次改接口前先冻结。4. 实操过程从零搭建一个可用的AIDL HAL服务4.1 服务实现类的编写接口和编译规则搞定后接下来写服务实现。在default/目录下创建YourDevice.h和YourDevice.cpp。先看头文件// YourDevice.h #pragma once #include aidl/com/yourcompany/hardware/yourdevice/BnYourDevice.h #include aidl/com/yourcompany/hardware/yourdevice/IYourDeviceCallback.h #include mutex #include thread namespace aidl::com::yourcompany::hardware::yourdevice { class YourDevice : public BnYourDevice { public: YourDevice(); ~YourDevice() override; ndk::ScopedAStatus getVersion(int32_t* _aidl_return) override; ndk::ScopedAStatus init(bool* _aidl_return) override; ndk::ScopedAStatus readData(std::vectoruint8_t* data) override; ndk::ScopedAStatus registerCallback( const std::shared_ptrIYourDeviceCallback callback) override; ndk::ScopedAStatus unregisterCallback( const std::shared_ptrIYourDeviceCallback callback) override; private: void dataThread(); int fd_; std::mutex lock_; std::shared_ptrIYourDeviceCallback callback_; std::thread thread_; bool running_; }; } // namespace几个关键点继承BnYourDeviceBn是Binder Native的缩写是AIDL工具生成的基类。你的实现类继承它并实现所有纯虚方法。ndk::ScopedAStatus这是AIDL C后端的返回类型。成功返回ndk::ScopedAStatus::ok()失败返回ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT)之类的。out参数用指针AIDL的out参数在C后端里是指针类型。比如int getVersion()对应ndk::ScopedAStatus getVersion(int32_t* _aidl_return)。回调用shared_ptr回调接口用std::shared_ptr 传递。再看实现文件的核心部分// YourDevice.cpp #include YourDevice.h #include fcntl.h #include unistd.h #include android-base/logging.h namespace aidl::com::yourcompany::hardware::yourdevice { YourDevice::YourDevice() : fd_(-1), running_(false) {} YourDevice::~YourDevice() { running_ false; if (thread_.joinable()) { thread_.join(); } if (fd_ 0) { close(fd_); } } ndk::ScopedAStatus YourDevice::getVersion(int32_t* _aidl_return) { *_aidl_return 1; return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus YourDevice::init(bool* _aidl_return) { fd_ open(/dev/yourdevice, O_RDWR); if (fd_ 0) { LOG(ERROR) Failed to open /dev/yourdevice: strerror(errno); *_aidl_return false; return ndk::ScopedAStatus::ok(); } running_ true; thread_ std::thread(YourDevice::dataThread, this); *_aidl_return true; return ndk::ScopedAStatus::ok(); } ndk::ScopedAStatus YourDevice::readData(std::vectoruint8_t* data) { std::lock_guardstd::mutex guard(lock_); if (fd_ 0) { return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); } >// service.cpp #include YourDevice.h #include android-base/logging.h #include android/binder_manager.h #include android/binder_process.h using aidl::com::yourcompany::hardware::yourdevice::YourDevice; int main() { ABinderProcess_setThreadPoolMaxThreadCount(0); std::shared_ptrYourDevice device ndk::SharedRefBase::makeYourDevice(); const std::string instance std::string() YourDevice::descriptor /default; binder_status_t status AServiceManager_addService( device-asBinder().get(), instance.c_str()); if (status ! STATUS_OK) { LOG(ERROR) Failed to register service: instance; return 1; } ABinderProcess_joinThreadPool(); return 0; }几个关键点ABinderProcess_setThreadPoolMaxThreadCount(0)设置binder线程池最大线程数为0表示不自动创建线程。然后调用ABinderProcess_joinThreadPool()进入循环。这是AIDL HAL的标准写法。AServiceManager_addService把服务注册到servicemanager。instance名通常是interface_name/default或者interface_name/instance_name。descriptorBnYourDevice::descriptor是AIDL工具生成的字符串就是package名加接口名。4.3 Android.bp编译服务在default/目录下创建Android.bpcc_binary { name: com.yourcompany.hardware.yourdevice-service, relative_install_path: hw, vendor: true, init_rc: [yourdevice-service.rc], vintf_fragments: [yourdevice.xml], srcs: [ YourDevice.cpp, service.cpp, ], shared_libs: [ libbase, libbinder_ndk, libcutils, liblog, libutils, ], static_libs: [ com.yourcompany.hardware.yourdevice-V1-ndk, ], cflags: [ -Wall, -Werror, ], }关键参数relative_install_path: hw安装到/vendor/bin/hw/目录下。vendor: true编译到vendor分区。init_rc指定init启动脚本。vintf_fragments指定VINTF声明文件。static_libs链接AIDL生成的ndk库。库名格式是interface_name-V -ndk。4.4 init.rc和VINTF声明init脚本yourdevice-service.rcservice vendor.yourdevice-service /vendor/bin/hw/com.yourcompany.hardware.yourdevice-service class hal user system group system seclabel u:r:yourdevice_service:s0VINTF声明yourdevice.xmlmanifest version1.0 typedevice hal formataidl namecom.yourcompany.hardware.yourdevice/name version1/version fqnameIYourDevice/default/fqname /hal /manifestVINTF声明告诉系统这个设备提供了com.yourcompany.hardware.yourdevice接口的1版本实例名是default。4.5 SELinux策略SELinux是最后一道坎。你需要为HAL服务定义domain并允许它访问设备节点和binder通信。在device/yourcompany/sepolicy/目录下创建yourdevice_service.tetype yourdevice_service, domain; type yourdevice_service_exec, exec_type, vendor_file_type, file_type; init_daemon_domain(yourdevice_service) allow yourdevice_service yourdevice_device:chr_file rw_file_perms; allow yourdevice_service servicemanager:binder { call transfer }; allow yourdevice_service yourdevice_service:binder { call transfer };还需要在file_contexts里给可执行文件打标签/vendor/bin/hw/com.yourcompany.hardware.yourdevice-service u:object_r:yourdevice_service_exec:s0以及给设备节点打标签/dev/yourdevice u:object_r:yourdevice_device:s0踩坑记录SELinux策略是最容易卡住的地方。如果服务起不来先看dmesg或者logcat里的avc denied。常见问题包括忘了allow servicemanager的binder call、忘了给设备节点打标签、domain定义和init.rc里的seclabel不一致。建议先用permissive模式跑通再收紧策略。5. 常见问题与排查技巧实录5.1 编译阶段常见错误错误信息原因解决方法cannot find interface com.yourcompany.hardware.yourdeviceaidl_interface的name和package名不一致检查Android.bp里的name和.aidl里的packageinterface changed but not frozen改了.aidl但没更新aidl_api按冻结流程更新aidl_api目录VintfStability not found忘了加VintfStability注解在接口和回调接口上都加unknown type IYourDeviceCallback回调接口没在同一个aidl_interface里把回调接口放在同一个srcs目录下5.2 运行时常见问题服务起不来先看三个地方init有没有启动服务、servicemanager里有没有注册、SELinux有没有拒绝。用ps -A | grep yourdevice看进程在不在。用service list | grep yourdevice看服务注册了没。用dmesg | grep avc看SELinux拒绝。如果进程在但服务没注册可能是AServiceManager_addService失败了。检查instance名是否正确以及servicemanager的SELinux策略是否允许。如果服务注册了但上层调不到检查VINTF声明是否正确。用dumpsys命令可以查看VINTF信息。5.3 回调不生效的排查回调不生效通常有几个原因回调对象被提前释放、回调线程和binder线程冲突、SELinux不允许binder通信。AIDL的回调是跨进程的上层注册的回调对象在HAL服务里是一个binder代理。如果上层进程死了回调代理会失效。HAL服务需要处理这种情况比如在onDataAvailable里捕获异常。另外回调调用时不要持有锁。我见过有人在dataThread里先lock再调callback结果上层回调里又调用了HAL的同步方法导致死锁。正确做法是先在锁内拿到callback的副本释放锁后再调用。5.4 性能优化的几个点AIDL HAL的性能瓶颈通常在binder通信和数据拷贝。如果数据量大考虑用shared memory或者ashmem传递而不是通过binder传byte数组。另外binder线程池的大小要合理设置。ABinderProcess_setThreadPoolMaxThreadCount(0)表示不限制但实际用的时候可以根据并发量调整。如果HAL服务要处理大量并发请求可以设置一个合理的上限。还有readData这种同步接口在高频调用时会有开销。如果可能尽量用回调方式推送数据减少上层轮询。6. 一些个人体会这个项目做下来最大的感受是AIDL HAL的文档虽然散但一旦跑通一个最小可用版本后面的扩展就顺了。我建议第一次上手时不要一上来就搞复杂的接口先写一个只有getVersion()和init()的接口把编译、启动、SELinux、VINTF全流程走通。这个最小版本跑起来后再加readData和回调心里就有底了。另外aidl_api目录的冻结流程一定要在第一次发布前就规划好。我见过有人接口改了七八次都没冻结最后编译系统直接报错只能把整个aidl_api目录删了重来。如果这个接口已经被其他模块依赖删了重来会导致依赖它的模块全部编译失败。所以第一次写接口时就要想清楚哪些方法是必须的尽量一次到位。最后分享一个小技巧调试AIDL HAL时可以用adb shell service call命令直接调用服务不用写上层App。比如service call com.yourcompany.hardware.yourdevice 1可以调用第一个方法。这个方法在快速验证服务是否正常时特别有用。
返回列表