ARTICLE DETAIL

资讯详情

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

Android音频HAL深度解析:从HIDL/AIDL到audio.bluetooth.default.so完整链路

Android音频HAL深度解析:从HIDL/AIDL到audio.bluetooth.default.so完整链路 做 Android 音频底层的几乎都跟这串文件名打过照面audio.primary.default.so、audio.a2dp.default.so、audio.usb.default.so以及今天的主角 audio.bluetooth.default.so。很多朋友第一次是在 out/target/product/xxx/vendor/lib64/hw/ 目录里撞见它们的第一反应是“这不就是普通动态库吗”结果真去读代码时才发现这玩意儿背后牵着一整套 HIDL/AIDL 接口定义和自动生成体系。本文不打算把整个音频栈铺开讲而是借 audio.bluetooth.default.so 这个具体文件把从 HIDL 接口定义、代码生成、AudioFlinger 加载链路到蓝牙音频通话路径的来龙去脉一次说清楚适合正在做系统音频、蓝牙音频或者准备入坑 HAL 开发的工程师参考。1. AOSP15音频HAL的整体架构so文件只是冰山一角1.1 从libhardware到HIDL再到AIDL的三次转身Android 8.0 之前音频 HAL 走的是 libhardware 那套老路线hw_get_module()按名字去 /vendor/lib/hw/ 里找 so然后通过module-common.methods这种 C 结构体回调拿到audio_hw_device_t、audio_stream_out_t等对象。那个年代的 HAL 实现基本是纯 C 代码文件名也五花八门比如常见的libaudio.so、audio.primary.board.so。Android 8.0 引入 HIDLHAL Interface Definition Language后接口定义从 C 头文件变成了.hal文件音频相关的接口被统一收敛到hardware/interfaces/audio/下面定义了IDevice、IStreamOut、IStreamIn、IStreamCommon这一系列抽象。HIDL 编译器会把这些.hal文件自动生成成 C 模板代码实现方只需要继承生成出来的IDevice基类把openOutputStream、setAudioPortConfig这些方法填上即可。到了 Android 13Google 又把 HAL 接口往 Stable AIDL 迁移音频 HAL 的接口包从android.hardware.audio2.0/4.0/5.0/6.0/7.0这套带版本号的 HIDL 包换成了不依赖版本号风格的android.hardware.audio.coreAIDL 包。AOSP15 里音频 HAL 已经以 AIDL 为主HIDL 更多是给老设备、老 vendor 做兼容保留。很多同学在这里有个误区以为 HIDL、AIDL 是两种“不同的 HAL 框架”。其实它们只是接口描述语言和传输绑定的差异底层的 so 文件依旧是同一个套路——编译出一个共享库系统按固定命名规则去固定目录加载。语言变了文件形态没变。1.2 AudioFlinger、HAL进程与运行库的分工音频 HAL 在 Android 里的加载方式跟普通 App 的 JNI so 完全是两个世界。ijkplayer 那类 so 是 App 进程自己dlopen、通过 JNI 调用而 audio.bluetooth.default.so 这种系统 so 的加载方是 audioserverAudioFlinger 所在进程加载入口在frameworks/av/services/audioflinger/的AudioFlinger::loadAudioInterface()。加载链路大致是AudioFlinger 先根据 device 类型primary、a2dp、usb、bluetooth 等拼出 HAL 模块名再通过libaudiohal、libaudiohalaidl这一层封装库去/vendor/lib64/hw/或/system/lib64/hw/里dlopen对应的 so。这些封装库内部会根据 VINTF manifest 里声明的 HAL 实现格式决定走 HIDL 的 hwservicemanager 还是 AIDL 的 servicemanager。需要特别注意so 文件只是“实现载体”真正让 AudioFlinger 和底层驱动/蓝牙栈对接的是接口协议。你把 so 理解成一台贴着接口说明书的机器AudioFlinger 不关心机器内部怎么运转它只按照接口说明书上的方法去调用。这就是为什么换一个厂商的设备只要仍然实现同一套 IDevice 接口AudioFlinger 就能无缝工作。2. HIDL那套东西到底是怎么工作的2.1 .hal 文件是如何变成C代码的HIDL 的核心就是.hal文件。以音频为例打开hardware/interfaces/audio/7.0/目录能看到IDevice.hal、IStreamOut.hal、IStreamIn.hal等一堆文件。它们长得很像 C 头文件但里面只有interface定义和方法声明没有实现。编译时hidl-gen工具会根据这些.hal文件生成两套代码一套是供 HAL 实现方继承的“桩”stub另一套是供 AudioFlinger 调用的“代理”proxy。中间的数据传输由 binder 完成接口方法名、参数类型、顺序都必须和.hal文件完全一致否则 binder 事务解析就会出错。这套代码生成机制是我觉得整个 HIDL 设计里最值得学习的部分——它没有像有些框架那样在运行时靠反射、解析配置去“猜”接口而是在编译期把所有接口固化到二进制代码里。对音频这种对时延敏感的模块编译期绑定能省掉一层运行时开销也更容易静态分析。AIDL 那套走aidl_interface的规则也是同理只是后端换成了 NDK。2.2 Binderized 与 Passthrough两种HAL模式的取舍HIDL 时代一个绕不开的概念是 HAL 有两种运行模式Binderized绑定式和 Passthrough直通式。Binderized 模式下HAL 实现在独立进程里跑AudioFlinger 通过 hwservicemanager 拿到 Binder 代理去跨进程调用。好处是模块崩溃不影响系统核心服务安全性高坏处是每次跨进程调用都有开销。Passthrough 模式则相反HAL so 不注册到 servicemanager而是被 AudioFlinger 直接dlopen进自己的进程地址空间接口调用完全不走 binder就是普通 C 函数调用。音频 HAL 绝大多数场景选 Passthrough图的就是低延迟。蓝牙音频 HAL 在很多实现里同样走 passthrough因为 A2DP 数据链路对时延和吞吐都有要求再叠一层 binder 拷贝实在不划算。到了 AIDL 时代这个划分依然存在只是名字换了。音频 AIDL HAL 的实现 so 同样可以被 audioserver 直接加载进进程VINTF 里的fqname会标明是哪个设备实例。2.3 AOSP15里HIDL的现状老接口还在新代码不写不少刚接触 AOSP15 源码的人会疑惑为什么我在代码里还能看到大量android.hardware.audio6.0、7.0的 HIDL 目录原因很简单兼容性。老设备的 vendor 分区里有大量按照 HIDL 接口实现的 HAL soGoogle 不可能在 Android 15 里直接砍掉这套接口否则老 device tree 全部要重写。所以你在源码里看到的 HIDL 音频包更多是“保留给旧实现”而新的参考代码、CTS/VTS 测试用例以及蓝牙音频 HAL 的 AIDL 实现已经全部切到新接口上了。如果是从头做新产品我强烈建议直接按 AIDL 路线走。HIDL 的维护成本不只是代码本身还有 hwservicemanager 的注册步骤、HIDL 版本升级时的 interface freeze 流程这些在 AIDL 里都简化了很多。不过读代码时还是要两套都看得懂因为现实世界的设备往往是 HIDL 和 AIDL 混着来的。3. audio.bluetooth.default.so的来龙去脉3.1 这个so到底是从哪段代码编译出来的很多文档只说 audio.bluetooth.default.so 是“蓝牙音频 HAL”但没讲它是谁编译的。在 AOSP 里它的最终归宿是 Bluetooth 模块的音频 HAL 目录packages/modules/Bluetooth/system/audio_hal/。Android.bp 里大致是这样定义这个模块的cc_library_shared { name: audio.bluetooth.default, relative_install_path: hw, proprietary: true, srcs: [ BluetoothAudioAidlHal.cc, BluetoothAudioSessionControl.cc, BluetoothAudioPortOut.cc, BluetoothAudioPortIn.cc, ], shared_libs: [ libbase, liblog, libhardware, android.hardware.audio.core-V7-ndk, libbluetooth_audio_session_aidl, libaudioutils, ], vintf_fragments: [ android.hardware.audio.core.bluetooth.xml, ], }编译产物就是vendor/lib64/hw/audio.bluetooth.default.so。如果你在设备上看到的文件名带别的后缀比如audio.bluetooth.xyz.so说明厂商没有直接用 AOSP 默认实现而是基于同一套接口做了自己的实现。这也是 HAL 设计的核心价值——接口固定实现自由。3.2 so文件里装的是什么IDevice和Stream的实现用nm -D或readelf -d打开这个 so你会发现它导出的符号并不多核心是IDevice的实现类。这个类在 AIDL 接口里叫android::hardware::audio::core::implementation::BluetoothDevice内部重写了 IDevice 的关键方法getAudioPort、getGain、setAudioPortConfig、openOutputStream、openInputStream。蓝牙设备的特殊性在于底层没有一个真实的 codec 设备。HAL 层看到的“硬件”其实就是蓝牙协议栈暴露出来的音频通道。所以 StreamOut 的write()方法不会去写 DMA、写 I2S而是把 PCM 数据通过蓝牙音频会话Audio Session送进蓝牙进程。AIDL 实现里有一个关键类BluetoothAudioPortOut它维护了一个BluetoothAudioSessionControl负责和蓝牙进程协商采样率、位深、通道数以及数据通路是“共享内存 Socket 通知”还是“纯 Socket 流”。从 Android 13 起默认实现使用共享内存做 PCM 缓冲socket 只用来传控制和事件通知这样大块音频数据不会在 binder/socket 上来回拷贝。3.3 音频从App到蓝牙耳机的完整数据路径把整条链路串起来大概是这样的App(AudioTrack) ↓ AudioFlinger(混音、重采样) ↓ 走 IDevice.openOutputStream 拿到流句柄 audio.bluetooth.default.so(HAL实现) ↓ memcpy 到共享内存 socket 通知 Bluetooth进程(蓝牙协议栈) ↓ A2DPSBC/AAC/LDAC编码LE AudioLC3编码 Controller芯片 ↓ RF 蓝牙耳机第一步不复杂App 创建 AudioTrack 后AudioFlinger 根据当前的音频策略把输出路由到对应的 HAL 设备上。当策略里选中的是蓝牙 A2DP 输出时AudioFlinger 打开的就是audio.bluetooth.default.so里的 StreamOut。第二步是很多人踩坑的地方蓝牙 HAL 的write()必须按蓝牙协议栈协商好的参数来写数据。协议栈跟 HAL 约定好采样率通常是 44100 或 48000、位深16 bit 为主、声道数立体声/单声道HAL 在getStreamConfig里把这些参数告诉 AudioFlingerAudioFlinger 混音器再按这个参数输出。如果你在实现里上报的 channel mask 和蓝牙栈实际协商出来的不一致就会出现声音变调、只有伴奏没人声或者干脆没声音。第三步是蓝牙进程内部的事。A2DP 下编码线程把 PCM 编码成 SBC/AAC/LDAC再按 L2CAP 的 MTU 分成包发给 controller。LE Audio 下则换成 LC3 编码走 CIS/ISO 链路。HAL 层对编码细节不感知它只负责把 PCM 稳定地喂给蓝牙进程。3.4 A2DP与LE Audio在HAL层面的差异AOSP15 的蓝牙音频同时支持 Classic A2DP 和 LE Audio。从 HAL 角度看两者最明显的差异体现在 DeviceAddress 和音频会话类型上。A2DP 场景下蓝牙音频会话的 type 约定为AUDIO_SESSION_A2DPHAL 侧通过对端蓝牙设备地址来区分是哪个耳机。LE Audio 场景下系统注册的是 BLE 音频输出设备会话类型变成AUDIO_SESSION_LE_AUDIO底层的音频编解码参数由蓝牙栈通过 Bluetooth Audio Control 接口下发。上一篇提到 LE Audio 的“多流”概念在 HAL 层也有体现同一个蓝牙 HAL 实例可能要同时服务来自多个 App 的流每个流走独立的StreamOut但底层共享同一条 ISO 链路。实现时要注意流之间的隔离别让一个流中断导致整条链路重建。另外 LE Audio 有很多参数是动态协商的比如 LC3 的 bitrate 可以随环境调整HAL 层要能响应setAudioPortConfig里的参数变化并及时反馈给 AudioFlinger。4. 实操编译、推送、调试一条龙4.1 单独编译audio.bluetooth.default.so的正确姿势整编一次 AOSP 动辄一两个小时改蓝牙 HAL 的时候没必要每次全编。在源码根目录执行source build/envsetup.sh lunch product-userdebug m audio.bluetooth.default -j$(nproc)编译产物在out/target/product/product/vendor/lib64/hw/audio.bluetooth.default.so。如果要放到 32 位目录对应vendor/lib/hw/得确认产品配置里编译了对应的 32 位版本。替换到设备上的标准流程adb root adb remount adb push out/target/product/product/vendor/lib64/hw/audio.bluetooth.default.so /vendor/lib64/hw/ adb reboot这里要提醒一句adb remount在 userdebug 构建上可用但正式 user 构建不能这么玩。另外替换前最好先备份原文件很多设备上的 vendor 分区空间很紧张push 失败是常事。如果你改了接口定义比如加了新方法那光编 HAL 不够还得编libaudiohalaidl、audioserver以及蓝牙进程那边的依赖整链路的编译范围会大很多。平时开发尽量别动接口只改实现。4.2 部署到设备后如何验证加载状态重启后第一件事先用lshal看接口注册情况adb shell lshal | grep -i audio正常输出里会有android.hardware.audio.core相关的实现以及它的设备实例列表。如果看到IDevice/bluetooth这个实例名说明蓝牙 HAL 已经被 VINTF 识别并注册了。这一步排查不了实际播放问题但能确认 HAL 有没有被系统“接纳”。第二步看 AudioFlinger 视角的 HAL 状态adb shell dumpsys media.audio_flinger | grep -i bluetooth这里能看到 AudioFlinger 为蓝牙设备打开的线程、采样率、buffer 大小、HAL 句柄信息。如果设备已经连接但这里没有对应的输出线程问题大概率在音频策略AudioPolicy没有把 A2DP/LE Audio 设备识别出来得去查audio_policy_configuration.xml里有没有声明 bluetooth 相关的 module 和 device port。第三步看蓝牙栈侧adb shell dumpsys bluetooth_manager | grep -i -E a2dp|le_audio确认蓝牙栈认为当前是 A2DP/LE Audio 活动状态并且编码参数已经协商完成。这一步经常能发现“HAL 正常但蓝牙侧没握手”这类跨进程问题。4.3 抓日志与排查问题的几个实用命令蓝牙音频问题涉及 audioserver、蓝牙进程、HAL 三个进程日志要分开抓。HAL 侧的核心 tag 是BluetoothAudioHALAudioFlinger 侧用AudioFlinger、AudioHAL蓝牙栈侧有BluetoothAudio、bt_btif、A2DP、LEAudio等。一条命令全部拉出来adb logcat -s BluetoothAudioHAL:V AudioFlinger:V BluetoothAudio:V A2DP:V LEAudio:V如果问题只在播放特定音频时出现建议开dumpsys media.audio_flinger里的 per-stream 信息看看是哪个 patch 链路上的 buffer underrun。蓝牙音频最典型的症状是声音断续这种场景十有八九是 HAL 喂数据不及时或者蓝牙进程消费太慢。可以先用dumpsys media.audio_flinger --threads看输出线程的 underrun 计数是不是在涨再决定调 HAL 的 buffer size 还是调蓝牙进程的编解码队列。还有一个容易被忽略的排查点SELinux。替换 vendor so 后经常出现权限问题日志里直接搜avc: deniedadb logcat -d | grep -i avc碰到 denied 就用audit2allow生成策略补丁千万别在生产环境直接setenforce 0糊弄过去。5. 常见问题与踩坑记录5.1 典型问题与排查思路速查表现象可能原因排查思路蓝牙耳机连接了但系统显示没有输出设备音频策略没识别蓝牙设备检查 audio_policy_configuration.xml 和 BluetoothAudioSession 状态lshal 里看不到 IDevice/bluetoothVINTF manifest 缺失或版本不匹配检查 so 对应的 vintf_fragments 是否被正确打包进 /vendor/etc/vintf/外放正常蓝牙无声HAL write 参数与蓝牙栈协商参数不一致对比 AudioFlinger 的 stream config 和蓝牙进程的 audio config蓝牙声音断续buffer underrun 或蓝牙编码队列拥堵dumpsys audio_flinger 看 underrun 计数调 buffer size替换so后开机 vendor 服务崩溃SELinux 策略未覆盖新文件或符号依赖缺失搜 avc denied用 ldd 检查 so 依赖链通话HFP声音异常HFP 场景的输入输出路由不同走了不同的 stream单独抓 in/out stream 日志确认 SCO 链路是否建立5.2 几条压箱底的心得做蓝牙音频 HAL 这几年我的体会是问题很少出在 HAL 自身的代码上更多出在“接口契约”不一致上。比如 A2DP 下 AudioFlinger 以为自己在写 48000/16bit/双声道但蓝牙栈协商完告诉 HAL 的是 44100/16bit/双声道两边对不上结果就是变速变调。这种问题光看 logcat 还不一定明显得同时对比 dumpsys 里的 HAL 上报参数和蓝牙栈的协商结果。第二个心得不要轻易在 HAL 里做重处理。蓝牙 HAL 的本质是搬运工不是处理器。有人喜欢在 HAL 里塞回声消除、音效增强表面看功能能跑但引入的处理延迟会直接打击用户体验还会让蓝牙音频的 VTS 测试很难通过。要加音效请在 AudioFlinger 的上层 effect 阶段加HAL 保持纯粹。第三个心得优先跑 VTS。AOSP15 的音频 HAL 有对应的 VTS 测试比如VtsHalAudioCoreTargetTest。在提交代码前先跑一遍能帮你把接口实现的边界问题提前暴露出来。蓝牙相关的还有BluetoothAudioHal的专项测试覆盖了 A2DP、Hearing Aid、LE Audio 三个 main session type。VTS 过了基本能保证 HAL 是“正确实现”VTS 不过大概率某个方法返回码或者 buffer size 没按 contract 来。最后再分享一个小技巧调试时把 so 替换到设备上之后先不要急着连蓝牙耳机先在 shell 里直接调用MediaPlayer播放一段测试音频确认 HAL 加载、路由都正常再连耳机。这样能把“HAL 本身问题”和“蓝牙链路问题”分开定位省掉大量反复配对的时间。
返回列表