ARTICLE DETAIL

资讯详情

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

Android P上添加自定义HIDL实例:从接口定义到SELinux的完整指南

Android P上添加自定义HIDL实例:从接口定义到SELinux的完整指南 前阵子帮同事在 Android P 底子上加了一个自定义的 HIDL 实例从接口定义到 SELinux 策略踩了一遍发现网上不少文章把“写 .hal 文件”和“真正跑起来”混在一起中间跳过了很多系统编排的关键步骤。正好借着这次实操把完整的添加流程掰开揉碎整理出来给后面要在 Android P 上动 HAL 层的同学做个参照。先说清楚这篇东西的边界我默认你已经能编译 Android 源码对 Hardware Interface Definition LanguageHIDL有一个模糊的印象知道它是 Treble 架构里连接 Framework 和 HAL 的契约层。如果你连 .hal 文件长什么样都没见过也没关系下面会从零开始把每一步讲透。1. 添加 HIDL 实例前先把整体设计思路理清楚1.1 为什么 Android P 上要优先用 HIDL 而不是传统 Binder 接口Android 8.0 之后系统强制推行 Treble 架构核心目的就是把 Framework 和 vendor 实现之间的耦合断开。传统做法是 HAL 直接注册 Binder 服务Framework 通过getService拿一个 Binder 对象两边编译期强依赖vendor 换个实现就得重新编译 Framework。HIDL 换了一种思路用 .hal 文件定义接口契约然后用 hidl-gen 工具自动生成 C 或 Java 代码Framework 侧和 vendor 实现侧各自独立编译运行期通过 hwservicemanager 按包名和版本号找到对应的服务实例。这样 vendor 升级硬件实现时只要保证接口版本不变Framework 可以完全不动。Android P 在 Treble 上的一个关键变化是强制要求 vendor 分区使用 VNDK 和 LL-NDKHIDL service 分发由hwservicemanager统一管理default manifest 和 vendor manifest 分开存放这在早期 Oreo 版本上还不那么严格。所以你会在 Android P 的源码里看到/vendor/manifest.xml和/vendor/etc/vintf/manifest.xml同时存在的各种改制版本具体用哪条路径取决于厂商有没有启用VINTF强制校验。1.2 一个 HIDL 实例在运行期到底是怎么被找到的理解“添加一个 HIDL 实例”究竟要做什么可以用一个生活化的比喻HIDL 服务像一个开在商业区的门店门店有招牌包名版本、有经营许可证manifest.xml、有提供的商品接口方法。客户端不知道门店的具体位置它只认招牌找到招牌后通过总服务台hwservicemanager拿授权然后才能进店消费。放在 Android P 的代码层一个可用的 HIDL 实例由四部分拼装而成IXXX.hal接口定义文件声明服务能干什么接口自动生成的代码负责 Binder 传输和序列化实现类继承生成的 stub真正干活的业务逻辑在这里一个注册入口告诉 hwservicemanager “我的名字是什么、对应的实现是谁”缺任何一环客户端都可能报Transport not found或者getService returns nullptr。这四步也是下面各个章节展开的主线。1.3 方案选型把自定义接口放在 hardware/interfaces 还是 vendor/ 目录很多第一次做 HIDL 的人会纠结接口放在哪里。Android P 源码里hardware/interfaces是 AOSP 官方接口的集中地比如android.hardware.audio、android.hardware.camera.provider。如果你的接口不需要上推给 AOSP 主线只是厂商内部使用我建议放到vendor/company/interfaces下比如vendor/foo/interfaces/demo。放 vendor 目录的好处是编译时可以和 AOSP 主线解耦后续升级源码不会因为路径冲突打补丁。缺点是hidl-gen生成代码时要多带一个-r参数指定根路径映射路径不对会生成一堆类似Could not find package android.hardware.demo1.0的报错这个后面实操部分会单独说。2. 环境准备与关键配置文件说明2.1 确认源码版本和编译环境动手前先确认两件事。第一你手里的 AOSP 分支确实是 Android P对应android-9.0.0_r*或厂商基于 P 的定制分支。第二源码已经完整编译过一次out/host/linux-x86/bin/hidl-gen这个工具已经存在于输出目录。因为 hidl-gen 本身也需要 build 出来如果你连lunch都没跑过后面所有步骤都走不下去。在 P 分支上我习惯用这种方式快速验证环境cd $AOSP_ROOT source build/envsetup.sh lunch your_product-userdebug which hidl-gen如果看不到 hidl-gen 路径手动跑一次make hidl-gen即可编译速度很快不用整包 rebuild。2.2 先认识 hidl-gen 的输入输出套路hidl-gen 是整个流程的核心工具它的常规用法是hidl-gen -o output-path -L language -r root-mapping packageversion-L参数决定生成什么类型的代码参数值生成内容什么时候用androidbpAndroid.bp 编译片段第一次生成代码时cppC 接口和实现模板基于接口生成实现javaJava 接口Java 层客户端使用c-headershidl 接口头文件依赖某个接口时实际操作时我会先跑androidbp再跑一次cpp。为什么分成两步而不是一条命令搞定因为分开跑方便检查接口最终的目录结构而且cpp生成时要求输出路径存在先跑androidbp可以借它创建的目录结构把框架搭好。2.3 VINTF manifest 的作用和格式要点Android P 上用 VINTFVendor Interface Object来描述系统能力manifest.xml 就是 VINTF 的静态描述文件。常见路径是vendor/etc/vintf/manifest.xml但很多厂商代码里还会保留老的vendor/manifest.xml如果两个文件都存在以 vintf 目录下的为准。manifest.xml 里描述 HIDL service 的方式大概长这样manifest hal formathidl namevendor.foo.demo/name version1.0/version interface nameIDemo/name instancedefault/instance /interface /hal /manifest有个细节值得注意instance不一定叫default你可以给它起任意名字比如primary、secondary客户端 getService 时要用同一个名字才能匹配上。这个设计的意义在于同一套 HIDL 接口可以启动多个实例处理不同硬件通道只是我在实际项目中见到的场景不多多数情况用default就够了。3. 实操第一步编写 .hal 接口定义文件3.1 包名、版本号与目录结构的关系HIDL 的包名不是随便写的它和目录结构、版本号强绑定。假设我要定义包vendor.foo.demo版本1.0接口IDemo那么 .hal 文件必须放在vendor/foo/interfaces/demo/1.0/IDemo.hal从vendor/foo/interfaces到demo是包名路径1.0是版本目录文件名和接口名必须完全一致。违反这个约定hidl-gen 就直接报错不存在运气好能跑过去的说法。3.2 一个能跑的 IDemo.hal 示例拿一个最简单的功能举例我给这个 demo 接口定义两个方法一个返回字符串一个做加法运算。package vendor.foo.demo1.0; interface IDemo { getDemoName() generates (string name); add(int32_t a, int32_t b) generates (int32_t result); };注意 HIDL 的方法语法和传统 AIDL 不同返回值用generates关键字来描述。如果方法要抛出异常HIDL 里通过callflow注解和错误枚举处理那属于更复杂的用法新手可以先不管。3.3 types.hal 里定义自定义数据结构实际业务里不可能全用 int 和 string比如我要给 demo 接口传一个坐标点就需要定义自己的结构体。HIDL 的套路是在同目录下建一个types.hal用struct声明package vendor.foo.demo1.0; struct Point { int32_t x; int32_t y; };然后在 IDemo.hal 里 import 这个类型package vendor.foo.demo1.0; import vendor.foo.demo1.0::Point; interface IDemo { getCurrentPoint() generates (Point point); };这里有个很多新手会踩的坑import 的包名必须和 package 声明保持完全一致哪怕前后的空格多了少了hidl-gen 都会报unknown type。建议代码里统一风格全部不加空格。3.4 定义接口时的几个设计经验根据我做过的几个 HIDL 接口总结接口设计阶段想清楚下面几件事后面能少返工很多一个接口只服务一个清晰的功能域不要建一个“万能接口”既管音频又管传感器客户端每次拿到的对象太重参数尽量用结构化类型而不是大量裸参数比如传Point而不是传两个int32_t后续版本演进时加字段只会升级 struct不用改方法签名更新版本时保留旧接口不要在原版本文件里乱动已经发布的方法最好的做法是建新目录1.1用extends继承旧接口同时新增方法4. 实操第二步用 hidl-gen 生成代码和编译框架4.1 生成 Android.bp 和 C 接口代码进入源码根目录执行下面的命令hidl-gen -o vendor/foo/interfaces/demo/1.0 -L androidbp \ -r vendor.foo:vendor/foo/interfaces \ -r android.hardware:hardware/interfaces \ vendor.foo.demo1.0 hidl-gen -o vendor/foo/interfaces/demo/1.0 -L cpp \ -r vendor.foo:vendor/foo/interfaces \ -r android.hardware:hardware/interfaces \ vendor.foo.demo1.0提示如果第二个命令报 “Output path exists but is not a directory” 之类的奇怪错误多半是第一个命令没把目录建对检查一下vendor/foo/interfaces/demo/1.0是否真的存在。生成结束后1.0目录下大致会多出这些文件Android.bp IDemo.hal types.hal IDemo.cpp IDemo.h BpDemo.cpp BnDemo.cpp DemoAll.cpp这些文件中BpDemo是客户端代理BnDemo是服务端 stubDemoAll是一个带HIDL_FETCH_IDemo的注册辅助文件后面实现服务端时用得上。4.2 编写接口对应的 service 目录和 Android.bp接口代码生成之后还要建一个单独的服务实现目录我习惯放在vendor/foo/demo-service/1.0/default/这里写Demo.cpp和Demo.h里面继承生成好的IDemoStub并实现所有方法。同时在这个default目录下放一个Android.bpcc_binary { name: vendor.foo.demo-service, relative_install_path: hw, proprietary: true, srcs: [ Demo.cpp, service.cpp, ], shared_libs: [ libbase, libhidlbase, libhidltransport, liblog, libutils, vendor.foo.demo1.0, vendor.foo.demo1.0-impl, ], }vendor.foo.demo1.0-impl是 hidl-gen 生成的接口库vendor.foo.demo-service是可执行文件落到设备后会在/vendor/bin/hw/目录下。relative_install_path: hw让可执行文件归到 hw 文件夹这是 Android 对 HAL 服务的约定目录便于权限管理。4.3 这一步最容易踩的坑编译目标名与 hidl_gen 生成规则不一致很多人在这里会遭遇undefined reference to IDemo::getService这类连接错误原因多半是 Android.bp 里shared_libs没写全或者链接的库名称和 hw 目录下的Android.bp不一致。一个稳妥的习惯是跑完 hidl-gen 后先打开生成的Android.bp看一眼里面会写明library_name、hidl_package等信息。例如hidl_interface { name: vendor.foo.demo1.0, root: vendor.foo, package: vendor.foo.demo1.0, types: [Point], }如果我在服务进程的shared_libs里少写了vendor.foo.demo1.0链接时十有八九报错。把生成出来的库名原样抄进依赖列表这个问题就能避开。5. 实操第三步注册 HIDL 服务并配置 SELinux 权限5.1 服务端入口怎么实现写service.cpp作为可执行文件入口里面做三件事创建设备服务对象、注册到 hwservicemanager、进入事件循环。核心代码大致是#include vendor/foo/demo/1.0/IDemo.h #include vendor/foo/demo/1.0/Demo.h using vendor::foo::demo::V1_0::IDemo; using vendor::foo::demo::V1_0::implementation::Demo; int main() { android::spIDemo service new Demo(); android::status_t status service-registerAsService(default); if (status ! android::OK) { ALOGE(Failed to register demoservice, status: %d, status); return -1; } ALOGI(demoservice is ready); while (true) { pause(); } return 0; }registerAsService(default)里的default就是客户端之后传给IDemo::getService(default)的 instance 名字。这里如果用getService()不传名默认也会找default实例所以很多代码里看到省略写法原理是一样的。5.2 在 manifest.xml 中声明服务能力服务进程跑起来还不够hwservicemanager 要能确认vendor.foo.demo这个包允许在设备上存在这就要改设备的 VINTF manifest。对于 Android P最稳妥的做法是在device/vendor/board/manifest.xml或vendor/etc/vintf/manifest.xml中加入manifest hal formathidl namevendor.foo.demo/name version1.0/version interface nameIDemo/name instancedefault/instance /interface /hal /manifest如果设备已经有统一的manifest.xml直接把hal块插进去即可。改完后重新打包 vendor 镜像否则客户端会报Could not find service vendor.foo.demo1.0::IDemo/default注意这个报错通常不是服务没有启动而是manifest 里没声明服务存在。5.3 SELinux 策略如何让服务注册和访问不被拦截SELinux 配置是 HIDL 实例能不能真正被访问的另一道门槛也是最容易让人抓狂的一步。你需要做三件主要的事第一给服务进程定义自己的 type。比如在device/vendor/board/sepolicy/vendor/demo.te里写type vendor_foo_demo, domain; type vendor_foo_demo_exec, exec_type, file_type, vendor_file_type; init_daemon_domain(vendor_foo_demo)第二告诉 SELinux 这个服务允许被别人访问同时它自己可以访问 hwservicemanagerbinder_call(vendor_foo_demo, servicemanager) hwservicemanager_binder_call(vendor_foo_demo) add_hwservice(vendor_foo_demo, hal_foo_demo_service) allow(hal_foo_demo_service, vendor_foo_demo, hwservice, write) allow(vendor_foo_demo, hal_foo_demo_service, hwservice, read)很多同学只配了allow却漏了add_hwservice结果客户端发起 getService 时被 SELinux 拒绝日志却显示在 servicemanager 这一环。正确的排查方式是抓到avc: denied事件后查看是缺add_hwservice的 permission 还是缺service_manager的add权限不要盲目加 allow。第三给文件打上正确的标签让 init 能拉起这个服务。在file_contexts里补/vendor/bin/hw/vendor\.foo\.demo(/.*)? u:object_r:vendor_foo_demo_exec:s0改完 SELinux 策略记得要重新编译 boot 镜像或 vendor 镜像并且在 userdebug 版本上可以用adb root adb shell setenforce 0临时验证服务能跑通再逐步去掉宽松权限。5.4 关于 init rc 文件Android P 上怎么配置最稳为了让服务开机自启还要给服务进程写一个.rc文件放在vendor/etc/init/下比如vendor.foo.demo.rcservice vendor.foo.demo /vendor/bin/hw/vendor.foo.demo class hal user system group system seclabel u:r:vendor_foo_demo:s0类名hal意味着它会在 HAL 相关的启动阶段被拉起。有个小坑是如果服务进程崩溃一次class hal的自动重启策略可能不会像class main那样严格所以生产环境可以考虑在 rc 里加oneshot或者自定义restart策略看业务需求。6. 实操验证与常见问题排查实录6.1 编译、烧录、验证的完整命令流所有文件改完后按下面的顺序编译make -j$(nproc) vendor.foo.demo-service make -j$(nproc) vendorimage烧录后进设备验证adb shell lshallshal列出了系统当前所有 HIDL 服务。如果能搜到vendor.foo.demo1.0::IDemo/default说明服务已经注册成功状态是REGISTERED那基本过了大半。之后再写一个小客户端调一下接口做功能验证。6.2 我实际开发中遇到的问题盘点为了节省大家排查时间把我在类似场景里遇到的高频问题整理成速查表错误特征可能原因解决方法getService returns nullptr服务没启动 / manifest 没声明 / instance 名不匹配先 ps -ATransport not found包名拼写错误或者 hidl-gen 没正确生成 transport 相关代码检查 hidl-gen 生成的文件是否齐全检查包名大小写avc: denied { add } for ... hwserviceSELinux 缺少 add_hwservice 规则在对应 .te 文件里补add_hwservice(vendor_foo_demo, hal_foo_demo_service)HIDL 接口编译时报unknown typeimport 路径写错或 types.hal 不在同目录检查 package 声明、import 语句确认 types.hal 放在版本目录下service vendor.foo.demo did not exit cleanly服务进程启动后崩溃抓 logcat 和 tombstone多半是动态库链接不全或空指针6.3 一条被很多人忽略的验证捷径直接用 LSHAL 做冒烟测试有些同学写完服务后急着写客户端代码其实在 Android P 上有个更快的验证办法。就是先确保setenforce 0的情况下服务能正常注册然后用adb shell lshal --typesall看服务是否已经列出再用一个小工具项目直接用IDemo::getService()调一下方法。如果返回正确再做 SELinux 收尾比一上来就全副武装要顺利得多。这里再分享一个实用技巧在 userdebug 版本上可以用 root 权限直接杀掉 hwservicemanager 测试系统重启服务的能力不建议在设备上这样操作因为 hwservicemanager 负责全部 HIDL 服务分发重启它会影响所有 HAL 服务并不是单独一个实例的测试场景。我在这里踩过一次坑想要测试单个服务崩溃恢复结果整个系统音频和摄像头服务全部跟着掉线。6.4 升级到 1.1 版本时旧实例怎么平滑过渡HIDL 版本演进是另一个经常被问到的话题。如果我的IDemo1.0已经稳定现在要扩展一个getVersionName()方法正确做法是新建1.1目录文件写package vendor.foo.demo1.1; import vendor.foo.demo1.0::IDemo; import vendor.foo.demo1.1::IDemo as IDemoV1_1; interface IDemo extends vendor.foo.demo1.0::IDemo { getVersionName() generates (string name); };这里不建议直接把 1.0 的接口文件拿过来改因为客户端可能还跑着 1.0 的老逻辑硬改接口属于破坏性变更。用extends继承旧接口新老版本并存是 GSI 和 Treble 社区最常见的兼容姿势。然后在 manifest.xml 里可以同时声明 1.0 和 1.1 两个版本hwservicemanager 会根据客户端请求的版本分发到合适的服务实例上。7. 个人心得这类功能后续还能怎么扩展写完这个 demo 服务后续再往别的方向延伸就比较顺手了。比如给服务加回传统线程模型用configureRpcThreadpool调整并发能力或者把服务改成 passthrough 模式通过HIDL_FETCH_IDemo加载动态库针对性处理那些不适合独立进程的轻量 HAL。这些都属于同一个知识树的进阶分支基础流程跑通了后面无非是查文档、试参数的功夫。回顾整个过程我最大的感受是“添加 HIDL 实例”这个任务真正难的从来不是写接口代码而是把 manifest、SELinux、服务注册这几块系统层面的拼图对齐。只要按接口定义、代码生成、服务实现、manifest、SELinux 的顺序一步步走大部分问题都能在早期暴露出来不会拖到最后烧录进设备才发现。如果按照这一整套流程走下来还有不顺的地方欢迎在评论区聊聊你在哪一步被卡住。我整理这些内容本身也是帮自己重新梳理思路Android P 版本的 Treble 细节和其他版本确实还有不少差别后面要是再碰到有意思的坑我再继续写。
返回列表