
插上耳机再按播放声音会立刻从耳机出来还是先从扬声器外放一下再切过去做播放器开发的同学大概率都遇到过类似问题明明代码里new了一个AudioTrack数据也write进去了可声音就是跑到了你没想到的地方。前阵子我排查一个“拔掉耳机后声音消失”的线上问题顺着AudioTrack的创建链路一路翻到AudioPolicyManager才把设备选择这件事彻底搞明白。这篇文章就把这条链路完整拆一遍主要基于AOSP Android 13主线代码涉及frameworks/av/media/libmedia/AudioTrack.cpp、frameworks/av/services/audioflinger以及audiopolicy相关源码读的时候留意版本差异就行。如果你是想搞懂Android音频路由audio routing原理的应用层开发、正在做音频策略定制的ROM工程师、或者被“插耳机仍然外放”这类bug折磨的调试人员这篇文章能帮你省掉大量翻源码的时间。设备选择并不是AudioTrack自己决定的它只是一份“请求单”真正做决策的是系统侧的AudioPolicyManager。1. 一条AudioTrack从创建到出声系统在哪一步定下了播放设备很多人以为“设备选择”发生在new AudioTrack那一刻其实不对。AudioTrack创建时拿到的是outputId而output和device是两码事。我用一个物流类比AudioTrack是一笔“音频包裹订单”里面装的音频数据是要寄出去的货PlaybackThread是一条“运输线路”device才是那个“最终收货地址”。系统先根据你的下单信息帮你选一条运输线路但货真正发车之后如果收货地址变了它还可以在途中改派。1.1 创建阶段getOutputForAttr先给AudioTrack找一个“发货口”先看AudioTrack::set这一段创建AudioTrack时真正干活的是这个函数// frameworks/av/media/libmedia/AudioTrack.cpp status_t AudioTrack::set(...) { ... audio_output_t output; status_t status AudioSystem::getOutputForAttr(attr, output, mSessionId, ...); ... }这个调用链会从libmedia进程一路走到audiopolicy服务进程。注意这里的关键点AudioTrack把自己身上携带的audio_attributes_t、uid、sessionId等信息统统交给了系统然后系统返回一个audio_output_t结构体。这个结构体里面包含outputId、采样率、格式、声道数、flags等但注意它不是一个直接的设备ID。这里有一条完整的调用链AudioTrack::set() - AudioSystem::getOutputForAttr() - AudioPolicyService::getOutputForAttr() // Binder跨进程 - AudioPolicyManager::getOutputForAttr() // 真正的路由决策 - AudioProductStrategy匹配 - getDeviceForProductStrategy() // 算出偏好设备 - findOutputRoute() / openOutput() // 找到或打开对应output也就是说设备偏好在getOutputForAttr阶段就已经算出来了但最终返回给AudioTrack的只是一个“发货口”。具体这个发货口最终通向哪个硬件设备后面还要看设备连接状态和策略。1.2 挂载阶段createTrack把AudioTrack挂到指定的PlaybackThread上拿到outputId之后AudioTrack继续调用IAudioFlinger::createTrackAudioFlinger这边会找到对应outputId的PlaybackThread在这个线程里创建Track对象// frameworks/av/services/audioflinger/AudioFlinger.cpp spIAudioFlinger::IAAudioTrack AudioFlinger::createTrack(...) { ... spPlaybackThread playbackThread checkPlaybackThread_l(outputId); ... track playbackThread-createTrack_l(client, streamType, attr, sampleRate, format, channelMask, ...); ... }这里我很早就踩过一个认知误区以为创建Track时指定的streamType或者audio attributes会直接决定设备后来翻代码才发现createTrack阶段只是把track挂载到一条输出线程上设备切换是播放过程中动态发生的。比如你创建AudioTrack时手机连着蓝牙系统返回的outputId对应的是蓝牙的output但如果接下来你拔掉蓝牙track并不会被销毁重建系统会在这个output内部切换底层设备甚至在你没感知的情况下把数据从蓝牙通道挪到扬声器通道。1.3 启动阶段startOutput才是设备真正烧火的时候数据准备好调用AudioTrack::start底层会触发PlaybackThread的start并回调到AudioPolicyService// AudioFlinger播放线程启动track后会向AudioPolicyService发起startOutput AudioFlinger::PlaybackThread::startOutput_l() - AudioSystem::startOutput() - AudioPolicyManager::startOutput()startOutput阶段会重新评估当前系统状态下这个output应该绑定的设备并且处理静音策略、设备变化等待处理事项。也就是说从你write数据到听到声音中间至少经历了“创建时选路”、“挂载时定线”、“启动时核对设备”三个阶段。等Track播放到一半插拔耳机系统又会触发新一轮重路由。2. AudioTrack携带的“身份信息”如何一步步传递给路由决策层既然AudioTrack自己不做设备选择那它靠什么影响系统选设备答案是audio_attributes_t。这个结构体是路由决策的核心输入很多做应用开发的同学对AudioAttributes不陌生但未必知道它在底层是怎么被消费的。2.1 AudioAttributes里真正影响设备选择的字段一段音频流从App层创建时不管你是用AudioTrack还是MediaPlayer最终都会生成一个audio_attributes_t结构体。里面有几个字段对路由选择影响最大usage代表用途比如AUDIO_USAGE_MEDIA、AUDIO_USAGE_ASSISTANT、AUDIO_USAGE_NOTIFICATION、AUDIO_USAGE_ALARMcontent_type代表内容类型比如音乐、语音、超声等flags一些特殊标志位比如AUDIO_FLAG_AUDIBILITY_ENFORCED会触发强制外放策略这个结构体在JNI层被转换成Native的audio_attributes_t后塞进AudioTrack::set再走上面的调用链传递给AudioPolicyManager。AudioPolicyManager会根据这些信息计算它属于哪个“产品策略”不同策略对应不同的设备偏好这就把应用层“我想放音乐”的诉求翻译成了“请优先走音乐设备路由”。2.2 getOutputForAttr在AudioPolicyManager内部怎么解码这些身份以Android 13主线为例AudioPolicyManager::getOutputForAttr并不直接switch usage它会先做一层属性扩展// frameworks/av/services/audiopolicy/common/managerdefinitions/src/AudioPolicyManager.cpp status_t AudioPolicyManager::getOutputForAttr(const audio_attributes_t *attr, audio_output_t *output, audio_session_t session, ...) { ... audio_attributes_t expandedAttr computeExpandedAudioAttributes(*attr); audio_product_strategy_t productStrategy getProductStrategyForAttributes(expandedAttr); ... }computeExpandedAudioAttributes会把历史遗留的streamType映射补充到attr里而getProductStrategyForAttributes则从系统配置的AudioProductStrategy列表中查找第一个能匹配当前attributes的策略。如果找不到就会退回老的routing_strategy计算逻辑。这里有个很重要的细节匹配顺序是“先动态策略后静态策略”。Android 10之后引入了AudioProductStrategy厂商可以单独配置一份product_strategy列表把不同usage/content_type组合映射到不同策略。如果你在framework层改过audio_policy_configuration.xml应该对这个有印象。2.3 setPreferredDevice到底动了哪根弦应用层如果想强制某个AudioTrack走特定设备AudioTrack暴露了setPreferredDevice接口。这个接口API 23就有了很多demo喜欢拿它做“强制外放”。但源码里看它并不是一个立即生效的硬开关// AudioTrack.cpp status_t AudioTrack::setPreferredDevice(audio_port_handle_t deviceId) { ... mPreferredDevice deviceId; if (mActive) { ... // 通知底层更新route } return NO_ERROR; }它设置的是Track对象自己的偏好设备底层在路由决策时会优先考虑这个偏好。但要注意这只是一个“偏好”不是“铁律”。如果上层策略、动态策略、设备连接状态和这个偏好冲突系统不一定会听你的。我在项目里就遇到过某个语音App为了强制走扬声器给AudioTrack设置了setPreferredDevice(SPEAKER)结果用户插着蓝牙耳机时始终不生效。查了源码才明白偏好设备是在所有候选设备中优先选择但如果当前状态这个设备不可用比如蓝牙A2DP独占策略会按默认顺序找下一个而不是强切。3. AudioPolicyManager的决策逻辑从产品策略到具体device的换算这一节是设备选择的核心也是整个源码分析里信息密度最大的部分。AudioPolicyManager里最关键的函数就是上一节提到的getOutputForAttr以及它的兄弟函数startOutput、setOutputDevice。设备选择最终会落到一个具体设备的整型枚举值上比如AUDIO_DEVICE_OUT_WIRED_HEADSET是0x4AUDIO_DEVICE_OUT_SPEAKER是0x2。3.1 AudioMix和动态策略先匹配“定制规则”再走默认策略Android音频路由支持动态策略应用可以申请通过AudioManager.registerAudioPolicy注册一个AudioPolicy把自己的AudioMix和特定usage或者uid绑定然后要求系统把匹配到的音频流路由到指定设备。这套机制在AudioPolicyManager::getOutputForAttr里的体现是// AudioPolicyManager::getOutputForAttr内部匹配动态策略 for (size_t i 0; i mPolicyMixes.size(); i) { if (audio_is_live_output_mix(mPolicyMixes[i]) ...) { // 命中AudioMix路由规则 device mPolicyMixes[i].mDeviceType; ... } }动态策略的优先级非常高一旦命中后面默认的产品策略计算就会被跳过。这也是为什么很多“抢麦”类App能通过registerAudioPolicy实现把系统声音路由到指定声卡而普通App完全做不到的原因——没有MODIFY_AUDIO_ROUTING权限根本注册不了policy。3.2 从产品策略到设备偏好一份典型的策略优先级表如果没命中动态策略系统会走“产品策略”计算。不同usage会映射到几个典型策略每个策略对设备连接状态的权重不同。我基于Android默认配置整理一份简化对照方便理解产品策略分类常见usage设备偏好优先级简化STRATEGY_MEDIAMEDIA、GAME、ASSISTANT等A2DP/LE Audio 有线耳机 USB 扬声器STRATEGY_SONIFICATIONNOTIFICATION、ALARM、SONIFICATION有线耳机 扬声器闹钟可能特殊处理STRATEGY_DTMFDTMF通话相关设备优先STRATEGY_CALLVOICE_COMMUNICATION通话耳机/听筒 蓝牙SCO 扬声器这份顺序不代表所有Android设备都一样厂商可以改但默认设计逻辑很清楚音乐追求音质和私密性优先选蓝牙铃声要保证可达性即便有蓝牙也常常往扬声器跑。设备偏好计算完后还要结合当前系统中已连接设备的DeviceVector做一次“可用性筛选”最终得到目标device。这里补充一个我在看代码时容易混淆的点Android源码里存在两套命名老代码用的是routing_strategyAndroid 10之后引入了audio_product_strategy_t。两者本质上都是“策略”但前者是硬编码枚举后者是可在XML里配置的数据驱动。遇到厂商定制机你看到日志里出现routing_strategy也正常因为很多代码仍然用策略索引在做事。3.3 startOutput到setOutputDevice设备选择真正影响硬件的那一下startOutput里面做完设备重新评估后如果发现当前output绑定的设备需要变就会调用AudioPolicyManager::setOutputDevice这条路径会最终走到AudioFlinger去设置播放线程的输出设备// AudioPolicyManager.cpp status_t AudioPolicyManager::setOutputDevice(...) { ... mpClientInterface-setOutputDevice(output-mId, device, force, address); }AudioFlinger收到之后会通知对应的PlaybackThread更新HAL层的设备参数。对HAL来说它关心的往往是“你让我用哪个设备输出、音频走哪个address”而不是关心你上层是哪个App在播放。所以在HAL层面看所有路由最后都简化为一个device枚举和若干参数。4. 耳机插拔与蓝牙切换设备变化如何触发旧track重路由前面讲的都是“创建/启动时的设备选择”但实际上生活中大部分问题发生在“播放过程中设备变了”。这一节单独讲重路由的触发链路因为这是线上bug最集中的地方。4.1 setDeviceConnectionState所有重路由的入口耳机插拔、蓝牙连接断开最终都会汇聚到AudioPolicyManager::setDeviceConnectionState。以有线耳机为例Android的AudioService通过WiredAccessoryManager检测到耳机插入事件后会调用AudioSystem.setDeviceConnectionState一路走到AudioPolicyManagerAudioPolicyManager::setDeviceConnectionState(...) - setDeviceConnectionStateInt(device, state, address) - 更新DeviceVector - handleDeviceConnectionState(device, state) - checkOutputsForDevice(devices, state, address)checkOutputsForDevice是一个很重要的函数它会遍历系统里所有output重新为每个output计算设备。只要某个output的现有设备和新算出来的设备不一致就会走setOutputDevice去切换。这个“遍历所有output”的机制保证了不仅正在播放的track能切设备连后台挂着的空闲output也会被重新绑路。4.2 A2DP与LE Audio蓝牙设备重路由的特殊性蓝牙这块是设备重路由的“重灾区”。传统A2DP的连接状态变化会走到AudioPolicyManager里对A2DP设备的特殊处理比如检查A2DP是否处于suspend状态、是否允许媒体流继续路由到扬声器等。LE Audio出现后又多了一套新的蓝牙音频设备类型但整体路由框架没有变只是设备枚举值和policy配置新增了条目。实际调试中你会发现蓝牙耳机断开瞬间往往不是立即切到扬声器而是存在短暂的音频中断。这个“中断”不是设备切换本身造成的而是A2DP的offload线程需要释放、新的输出线程需要建立数据通道要重新铺。源码层面AudioFlinger的PlaybackThread在处理设备切换时会对track做一次短暂暂停和恢复。4.3 重路由对老track的影响track没变设备已经变了重路由机制带来的一个直接后果是同一个AudioTrack对象你调用getRoutedDevice查询时前后的返回值可能完全不同而你的track对象本身没被销毁重建。Android为此提供了两类回调AudioTrack.OnRoutingChangedListener针对单个trackAudioManager.AudioDeviceCallback针对系统设备变化我建议做播放器的同学凡是涉及外设切换的UI逻辑都优先监听回调而不是在播放出错时才去查询设备。因为设备切换时底层的Write不一定报错只是数据被“偷偷”送到了另一个device你不监听回调UI状态就一直是脏的。5. 实战排查如何凭借源码线索快速定位“选错设备”的问题最后这部分讲几个我实际排查设备选择问题时的套路。很多人一遇到“声音从不对的设备出来”就想当然去改应用代码其实90%的情况先查系统路由状态就能定位。5.1 用dumpsys看清系统当前路由状态Android系统提供了几个调试命令设备选择问题最常用的是这两个# 查看AudioFlinger各输出线程和设备绑定情况 adb shell dumpsys media.audio_flinger # 查看音频策略、设备连接状态和策略配置 adb shell dumpsys audiodumpsys media.audio_flinger的输出里找到“Output threads”段落能看到每个PlaybackThread绑定的设备。设备通常以十六进制枚举形式出现比如0x2代表扬声器、0x4代表有线耳机、0x80代表蓝牙A2DP。这个枚举定义在system/media/audio/include/system/audio.h里排查时对着宏定义看即可。dumpsys audio里则能看全局的设备连接状态哪个设备connected、哪个disconnected一目了然。我排查“耳机插入仍外放”问题时第一步就是先看这里如果耳机状态在系统层都没置为connected那根本不用往下查App问题出在硬件上报或AudioService。5.2 logcat里值得关注的三类日志设备选择过程在logcat里会打大量日志关键是知道看哪些tagAudioPolicyManager会打getOutputForAttr、setOutputDevice、setDeviceConnectionState的调用日志能看到设备枚举值AudioFlinger会打PlaybackThread相关的设备切换、线程创建日志AudioService会打设备连接事件、权限判断日志常见的组合命令adb logcat -v threadtime | grep -E AudioPolicyManager|AudioFlinger|AudioService如果一个问题能在logcat里看到setDeviceConnectionState已调用、但AudioPolicyManager没有重路由那问题大概率在policy策略计算如果能重路由但声音还是不对问题则可能在HAL层或output选择上。5.3 我实际踩过的三个设备选择坑这里分享三个有代表性的问题都是源码层面才能解释的。第一个是“设置了setPreferredDevice但没生效”。前面讲过偏好设备只是候选优先级不是硬性指定。我那次是App想强制声音从扬声器出来但当时蓝牙耳机还在A2DP连接中系统策略里音乐类usage的蓝牙优先级高于扬声器于是偏好失效。解决方向不是继续在应用层加力而是要把蓝牙断开或改走音频话题协商。第二个是“插拔耳机导致播放中断”。问题出在App拿旧设备信息做了缓存A2DP断开后系统自动切到扬声器App却因为回调没接住界面还停留在蓝牙耳机状态。代码里做了个缓存判断把AudioTrack.getRoutedDevice的结果存住不超过几秒导致用户在耳机断开瞬间操作出错。这个是多设备场景下很典型的业务bug根因就是对设备选择动态性认识不足。第三个是“多个Track同时播放设备被低优先级Track带偏”。源码里不同策略的track可能挂在同一条mixer线程上当一个高优先级震动/铃声track启动时系统按sonification策略把output切到了耳机结果后台音乐的track也被顺带路由到了耳机。这个不是系统bug而是共享output的副作用应用层要提前区分自己的track归属必要时使用独立output或者通过setPreferredDevice做隔离。最后说一个调试习惯设备选择这类问题靠看代码“猜”效率很低我现在的习惯是遇到路由问题先抓三样东西当前output线程设备绑定关系、设备连接状态的完整dump、AudioPolicyManager的日志。三样对齐之后问题基本在十分钟内能定位到“是策略问题、设备上报问题、还是应用使用错误”。毕竟AudioTrack只是送了个“请求单”系统侧怎么判才是关键。理清这条链路之后再看那些插拔耳机、切换蓝牙的诡异bug心里就有底了。