尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android音频设备选择全解析:AudioTrack路由策略与调试

Android音频设备选择全解析:AudioTrack路由策略与调试 Android 音频系统里AudioTrack 是每个做播放器都绕不过去的类。无论你用 MediaPlayer、SoundPool 还是 ExoPlayer最终送 PCM 数据给底层的基本都会落到 AudioTrack 上。但很多人调试到一半会遇到这类问题明明插了耳机声音却从扬声器出来蓝牙连上了媒体播放还是没有声音切歌或者插拔设备的时候声音延迟一大截。这些现象的根源往往不是应用层写错而是 AudioTrack 在设备选择这一步决策出了问题。这篇文章把我梳理 AOSP 源码时的经验整理出来围绕 AudioTrack 从创建到最终落在哪个实际输出设备上的完整链路展开拆解设备选择的策略逻辑和调试方法适合正在看 Android 音频 Framework 源码、或者正在排查音频路由问题的同学参考。1. 先搞清 AudioTrack 在音频系统里的定位1.1 AudioTrack 不只是“播放声音”的类很多刚接触源码的人会把 AudioTrack 理解成一个音频数据的发送器setPcmData 进去声音出来任务结束。但从系统视角看这件事没这么简单。AudioTrack 本质是客户端代理它在创建时要告诉系统“我要播什么类型的声音、给谁听、允许多大延迟”系统根据这些信息决定“从哪条路把数据送出去”也就是输出设备。这个链路牵扯到三层应用层只见到 AudioTrack 的操作接口中间是 Android 的音频服务端 AudioFlinger负责管理所有正在播放的 track 和输出线程再往下是 AudioPolicyManager职责之一就是做设备路由决策。设备选择的核心决策发生在 AudioPolicyManager但 AudioTrack 的创建过程是触发的起点。从源码目录看AudioTrack 客户端实现在frameworks/av/media/libaudioclient/AudioTrack.cpp旧版本可能在libmediaAudioPolicyManager 在frameworks/av/services/audiopolicy下面AudioFlinger 则单独在frameworks/av/services/audioflinger。梳理路径时这几个目录会反复跳转。1.2 设备选择不是“遍历声卡”那么简单有些人会以为让哪个设备发声就是程序遍历一遍当前可用的设备列表选一个硬件标识塞进去就行。真正看源码后会发现Android 的音频输出设备是“策略驱动”的同一个时刻可能有很多设备处于连接状态比如扬声器、有线耳机、蓝牙 A2DP、USB DAC、HDMI但它们是否可用、优先级多少、是否允许同时输出都是由策略决定的。举个例子系统铃声播放时即使你把媒体音量调成静音铃声可能照样响因为铃声走的是独立策略不允许被普通媒体音量完全压制再比如车载场景导航提示音和音乐可能同时存在但导航要走媒体通道还是单独通道也是策略设计时定的。这些在应用层看不见全在 AudioPolicyManager 的策略判断里。所以理解 AudioTrack 的设备选择本质上是在理解 Android 音频系统的路由策略。下面从调用链开始逐个拆。2. 从应用创建到系统决策设备选择的完整调用链2.1 应用创建 AudioTrack 时带了哪些“路由线索”用标准的 AudioTrack 创建方式举例AudioAttributes attrs new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); AudioTrack track new AudioTrack.Builder() .setAudioAttributes(attrs) .setAudioFormat(audioFormat) .setBufferSizeInBytes(bufferSize) .setTransferMode(AudioTrack.MODE_STREAM) .build();很多人写到这里就结束了没留意 AudioAttributes 的作用。实际上usage、contentType、flags都是后面系统做策略映射的重要输入。USAGE_MEDIA会被映射到媒体策略USAGE_ASSISTANCE_NAVIGATION_GUIDANCE可能走导航通道USAGE_VOICE_COMMUNICATION则可能走通话通道。应用创建的 AudioTrack 只是一个“出声请求”系统拿到这些线索之后才开始决策。在AudioTrack.cpp的构造函数里最终会调用到核心的AudioTrack::set(...)。这里会校验采样率、通道数、格式并触发与音频服务端的第一次交互。2.2 getOutputForAttr 是设备选择的“申请入口”AudioTrack::set 中最关键的几步是获取一个有效的 output 句柄再根据这个句柄去创建真正用于播放的 track。源码里可以找到类似这样的调用// frameworks/av/media/libaudioclient/AudioTrack.cppAndroid 10 路径 status_t AudioTrack::set(...) { ... audio_io_handle_t output AUDIO_IO_HANDLE_NONE; audio_port_handle_t portId AUDIO_PORT_HANDLE_NONE; status AudioSystem::getOutputForAttr( mAttributes, mSessionId, mStreamType, mClientUid, mClientPid, output, mFlags, mOutputFlags, portId); ... }getOutputForAttr会通过 Binder 最终到达 AudioPolicyManager。整个过程会走好几个中间层但核心入口是AudioPolicyManager::getOutputForAttr。这个方法会判断当前音频属性适合用哪一个output没有匹配的 output 就尝试创建有则直接复用。这个“匹配”过程就是设备选择的核心。决策完之后应用侧拿着返回的output句柄调用AudioFlinger::createTrack此时 AudioFlinger 会根据这个句柄找到合适的PlaybackThread把客户端 track 挂到真实输出线程上。输出线程再往下通过 HAL 写音频数据声音才真正出来。2.3 createTrack 之后设备还可能在播放中动态切换第一次创建 AudioTrack 时选中的设备不是一成不变的。比如你在听歌时插入耳机Android 的策略会立刻触发路由切换把当前正在播放的 track 从扬声器切换到耳机。这个切换不是应用主动做的事而是 AudioPolicyManager 发现设备状态变化后重新给已经打开的输出设备分配路由。源码上这个重路由逻辑很重checkOutputForAllStrategies会根据最新的可用设备集合重新调用setOutputDevices最终驱动 AudioFlinger 的输出线程threadLoop应用新的 route。所以在 grep 源码时不要只盯着getOutputForAttr还要关注setDeviceConnectionState和checkOutputForAllStrategies这两个方向才能把完整闭环串起来。3. 核心决策层AudioPolicyManager 的策略逻辑3.1 不同类型的音频走不同的策略组源码里有一个细化函数getStrategyForAttr它负责把 AudioAttributes 映射到 Routing Strategy。常规 AOSP 中能看到的策略大致有这些不同 Android 版本名字和实现有差异但思路稳定Strategy典型使用场景常见可路由设备STRATEGY_MEDIA音乐、视频、游戏等媒体声音扬声器、有线耳机、蓝牙 A2DP、USB、HDMISTRATEGY_SONIFICATION系统提示音、闹钟、部分按键音扬声器优先也支持耳机STRATEGY_PHONE语音通话相关听筒、有线耳机、蓝牙 SCOSTRATEGY_DTMF拨号音、按键 DTMF听筒、耳机、扬声器STRATEGY_ENFORCED_AUDIBLE强制外放场景扬声器等每个策略都维护了一个“期望设备集合”。设备选择的过程并不是在所有设备里选一个最好的而是先在当前策略允许的设备集合里过滤再按照优先级挑出当前可用的输出设备。AOSP 默认实现里会有很长的条件判断比如判断蓝牙是否开启 SCO、是否有线耳机插入、USB 是否占用等最终返回一个设备类型比如AUDIO_DEVICE_OUT_BLUETOOTH_A2DP或AUDIO_DEVICE_OUT_WIRED_HEADSET。这里有一个容易踩的坑不要以为设备集合越大越好。如果某个策略把不该走的设备也加进去比如让媒体声音可以走到通话听筒后面设备切换时就会出现诡异的“从听筒放歌”问题。因此在定制系统时很多人会改动这个策略映射但改之前必须完全理解每个设备类型的定位。3.2 getDeviceForStrategy 的优先级怎么定策略内部再往下是getDeviceForStrategy这类函数它负责在策略允许的设备集合里挑一个“最佳设备”。源码里会看到大量 if-else判断顺序其实就是优先级顺序。大致规律是有线设备通常优先于无线设备比如插入有线耳机时很多场景会优先走耳机而不是蓝牙蓝牙 A2DP 在媒体场景通常优先于扬声器但前提是 A2DP profile 启动了并且设备处于 active 状态通话策略里蓝牙 SCO 优先级往往高于听筒这样车载或蓝牙耳机通话场景才有意义特殊场景下flags 会直接改写设备选择比如AUDIO_FLAG_LOW_LATENCY可能更倾向选择低延迟的 output而不是最大响度设备。判断顺序背后是产品逻辑越接近用户当前意图的设备优先级越高同时避免声音“放不出来”。因此拿到一份设备不按预期输出的 bug 时先通过日志确认是走到了设备集合过滤之前还是过滤之后这样能快速缩小问题范围。3.3 动态路由切换的实现思路设备插拔事件进来后AudioService 会调用AudioSystem::setDeviceConnectionState最终 AudioPolicyManager 更新内部维护的可用设备集合再对现有策略做一次重评估。如果策略的目标设备变了已经打开的 output 就会做路由切换。这个切换过程不是瞬时的。AudioFlinger 的输出线程在threadLoop里周期性地处理路由请求切换过程中还要处理音频数据的清零、缓冲、HAL 重配。这也是为什么插拔耳机或切换蓝牙时经常能听到短暂“啪”一声或一瞬间的卡顿。很多做车机定制的团队会针对这个路由切换做专门的优化例如预启动多个输出线程、提前打开 A2DP output但代价是内存和耗电上升。4. 实操向用源码姿势排查设备选择问题4.1 用 logcat 锁定路由决策现场排查设备选择问题第一步是抓 logcat相关 tag 主要有audiopolicy、AudioPolicyManager、AudioFlinger、audio_route。实际操作时建议先清理缓存再复现操作adb logcat -c # 然后执行触发问题的操作例如插耳机或切换播放 adb logcat -v threadtime | grep -E AudioPolicyManager|audiopolicy|AudioFlinger|audio_route关注两个关键位置第一setDeviceConnectionState是否被调用设备连接事件是否被 system server 正确转发第二getOutputForAttr返回的 output 和标志位是否符合预期。很多问题在日志里一眼就能看出来比如插了耳机但日志没有任何 headset 相关事件说明问题根本不在 AudioTrack而是上层设备状态上报断了。反过来如果事件正确但策略返回的还是扬声器则要往策略判断逻辑上查。4.2 用 dumpsys 查看当前输出设备状态和路由映射只靠 logcat 还不够动态状态下用 dumpsys 看全局更可靠。常用命令是adb shell dumpsys media.audio_policy adb shell dumpsys media.audio_flingermedia.audio_policy的输出中包含当前所有 output、device、profile 的绑定关系能看到哪些设备处于连接状态哪个 output 的 active device 是什么。media.audio_flinger则能看到当前打开的 PlaybackThread、采样率、通道数等信息。两者配合基本能拼出完整的数据通道。有一个个人习惯把问题复现前的dumpsys和复现后的dumpsys都保存下来直接 diff。设备选择问题往往是一瞬间状态错乱对比文件能帮你快速定位是哪个设备状态位变了。4.3 应用层快速验证setPreferredDevice 是绕行法宝如果只是想快速验证“是不是设备选择策略导致的问题”可以在应用层强制指定想要输出的设备不用改系统代码。API 26 之后AudioTrack 提供了setPreferredDevice方法AudioDeviceInfo speaker findDevice(AudioManager.GET_DEVICES_OUTPUTS, TYPE_BUILTIN_SPEAKER); if (speaker ! null) { track.setPreferredDevice(speaker); }这只是绕过策略实际播放时系统仍会做合法性检查。但它用来做 A/B 验证非常高效指定扬声器有声音、不指定就走错设备说明策略选择有问题指定了依然没声音那问题大概率出在更底层的 HAL 或音频通路配置上。5. 常见路由问题与排查速查表5.1 高频设备选择问题整理下面这张表是我在实际项目里遇到过的典型问题以及排查方向不一定覆盖所有版本但思路通用。现象可能原因排查建议插入有线耳机后声音仍走扬声器耳机插入事件未触发设备状态上报或 policy 配置缺少 headset 设备检查 logcat 中 setDeviceConnectionState 是否出现dumpsys media.audio_policy 查看 Wired headset 是否已连接蓝牙连接成功但媒体无声A2DP 未处于 active或策略未把媒体路由到 A2DP查看 dumpsys 中蓝牙 A2DP profile 状态以及 strategy 设备集合是否包含 A2DP播放特定 App 声音总是走错设备AudioAttributes 的 usage 设置不合理被映射到其他策略让 App 开发者提供 AudioAttributes 配置用 logcat 确认策略映射USB DAC 插入无声USB 设备被系统识别但未加入可用输出设备查看 audio_policy_configuration.xml 中 USB mixPort 和 devicePort 是否配对切换设备时有明显延迟或啪声路由切换时 HAL 重新打开设备线程重建从 dumpsys audio_policy 观察线程切换情况优化策略提前预打开常用设备5.2 一个具体案例蓝牙播放总是没有声音之前遇到过一个问题配对蓝牙耳机后系统 UI 显示连接成功但媒体播放始终没声音。从 logcat 看AudioPolicyManager::getOutputForAttr返回的 output 已经有值但setOutputDevices始终把设备设为AUDIO_DEVICE_OUT_SPEAKER。进一步 dumpsys audio_policy 发现蓝牙耳机在mAvailableOutputDevices里没有出现也就是策略层根本没有感知到这个蓝牙设备。排查到最后是定制系统的audio_policy_configuration.xml里蓝牙 A2DP 的 devicePort 没有挂到 media 相关的 mixPort 上导致设备虽然连接了但对策略不可见。解决办法不是改 App 代码而是修正配置里的 route让 A2DP device 可以从 media 的 output mixPort 路由出去。这个案例说明遇到设备选择问题先不要怀疑代码逻辑优先确认系统“知道不知道”这个设备存在。6. 个人体会梳理设备选择流程时值得注意的坑6.1 源码版本差异是最大的坑AOSP 近几个大版本的音频代码重构频率很高同一个函数的位置、参数、责任范围变化非常大。比如AudioPolicyManager在旧版本和新版本之间拆分成了不同服务getOutputForAttr的调用链中间可能多了一层 Binder 接口。写分析文章也好实际排查也好一定要以你当前树内源码为准千万不要拿网上的旧文章直接套新代码。经验是先在工程里全局搜索getOutputForAttr和checkOutputForAllStrategies把调用链完整拉出来再往下读。6.2 设备选择从来不只是技术问题源码里的策略默认值是 Google 对通用 Android 设备的假设。但到了车机、电视、音箱、对讲机这些产品上默认策略往往不满足需求。比如车机上音乐和导航需要混音电视上 HDMI 回传和蓝牙音箱可能是竞争关系这些不能只靠 App 层 setPreferredDevice 救急而是要从设备端口、mixport、策略映射、动态路由这些系统层面整体设计。我的习惯是在改任何路由策略前先把 devicePort、mixPort、route 三者的关系画成一张静态连线图再对照dumpsys media.audio_policy输出的动态关系确认改了配置后不会影响到其他策略。否则很容易出现“修好了 A 问题B 场景又开始乱切设备”的连锁反应。整套源码分析下来AudioTrack 的设备选择就像整个音频系统的交通调度中心所有声音都要从这里选路弄懂它很多疑难音频问题就成功了一大半。
返回列表