
1. 设备选择问题的真实场景与核心链路先从一个我实际调试过的现场说起。有个播放器项目用户反馈在Android 11手机上插上3.5mm耳机后声音还是从扬声器出来我们通过AudioTrack.setPreferredDevice()指定了有线耳机设备日志里getPreferredDevice()也返回了预期设备但出声设备就是不对。查了几天最后定位到问题根本不在应用层而是整个AudioTrack设备选择链路的一环被绕过了。这个case让我意识到很多做播放器、做Framework定制、做车机音频的开发者对AudioTrack的“设备选择”理解都停留在API层面以为调了setPreferredDevice就万事大吉。实际上这个接口只是把你想要的设备作为“期望值”传到底层底层AudioPolicyManager有一整套策略来决定最终用哪个输出设备你的期望可以被接受也可以被忽略甚至可以在播放过程中被随时改变。这篇内容适合三类人看一是做音频播放器、音视频SDK遇到设备路由诡异问题的应用层开发者二是做Android Framework或HAL定制需要深入AudioPolicy引擎的系统开发三是刚接触Android音频源码、想理清AudioTrack到AudioFlinger再到AudioPolicyManager这条调用链的学习者。我会从源码调用路径、设备路由策略、常见问题排查三个维度来拆尽量把这条链路讲透。这条链路的核心其实就一句话AudioTrack只是发起方真正的路由决策权在AudioPolicyManager手里。你指定设备底层策略引擎会根据策略、设备状态、输出能力来“重新算一遍”算出最终设备然后建立或匹配对应的PlaybackThread和Output。搞清楚这个“重新算一遍”的规则所有诡异问题就都有了解释。2. 源码调用链device_id到底是怎么穿越Client和Server的2.1 Java层到Native层期望设备如何进入Track先说应用侧的入口。AudioTrack.java里setPreferredDevice(AudioDeviceInfo deviceInfo)最终调用的是native方法进入libaudioclient库的AudioTrack类。在native构造时核心函数是AudioTrack::set()而设备ID作为audio_attributes_t的一部分或者独立参数被传入。从源码看set()内部会先把各种参数采样率、声道、format、flags、deviceId整理好然后调用createTrack_l()。createTrack_l()做的事很关键它负责和AudioFlinger建立连接申请创建真正的Track对象。这个函数会组装一个audio_track_cblk_t共享内存块同时把请求参数通过IAudioFlinger::createTrack()Binder调用发到system_server进程的AudioFlinger服务。这里有个容易忽略的点setPreferredDevice设置进去的deviceId在native层会写入mAttributes的mDeviceId字段并在createTrack_l时作为参数传给AudioFlinger。但是这个参数在AudioFlinger那边并不会被直接当作“最终输出设备”它只是被原样带下去交给后续策略引擎判断。所以从这个角度看setPreferredDevice更像是给底层策略引擎的一个“参考意见”。// AudioTrack.cpp 简化逻辑 status_t AudioTrack::set() { ... if (deviceId ! AUDIO_DEVICE_NONE) { mAttributes.mDeviceId deviceId; } ... return createTrack_l(); }createTrack_l()里有一段特别关键它决定了Track走哪种创建路径。如果请求的是Offload播放比如AUDIO_FLAG_COMPRESS_OFFLOADAudioTrack会尝试创建DirectTrack如果是普通播放创建常规Track。设备和输出选择的差异也正是从这开始分叉的。2.2 AudioFlinger侧createTrack如何处理设备参数AudioFlinger的createTrack()收到客户端请求后不会立刻为这个Track选一个PlaybackThread。它会先分析参数调用AudioPolicyService的getOutputForAttr()来决策输出。这里展示一下AudioFlinger::createTrack()的核心逻辑路径它会根据请求的audio_output_flags_t和属性从当前已存在的PlaybackThread列表中寻找匹配的输出如果找不到匹配输出则调用AudioPolicyManager创建新的输出。这个“匹配输出”的判断条件非常严格采样率、通道数、format、flags、设备都要对齐。我们之前遇到过一个情况App设置了AUDIO_FLAG_FAST请求快速路径但指定的采样率和当前PlaybackThread不匹配系统干脆给它新建了一个独立的fast mixer线程。线程是新建了设备却还是用的线程默认设备不是App期望的那个设备。这暴露出一个问题fast路径下设备往往由线程绑定而不是由track决定。2.3 设备决策的“临门一脚”getOutputForAttrgetOutputForAttr()是整个设备选择链路里最核心的函数定义在AudioPolicyManager中。它的调用时机有两个一是Track创建时二是系统策略变化如插拔耳机、蓝牙连接时。它接收的属性包括audio_attributes_t包含usage、content_type等信息、audio_config_t采样率、通道数、format、请求的flags和设备ID然后输出一个最终的audio_io_handle_t输出句柄和设备ID。// AudioPolicyManager.cpp 关键路径 status_t AudioPolicyManager::getOutputForAttr( const audio_attributes_t attr, audio_io_handle_t* output, audio_session_t session, audio_stream_type_t* stream, uid_t uid, const audio_config_t* config, audio_output_flags_t* flags, audio_port_handle_t* selectedDeviceId, bool* isFormatSupported) { ... // 根据usage计算routing strategy routing_strategy strategy getStrategyForAttr(attr); ... // 根据strategy和设备状态选择输出设备 audio_devices_t device getDeviceForStrategyAndClass(strategy, ...); ... // 查找或创建匹配的output *output getOutputForDevices(device, session, stream, config, flags, ...); ... }注意getStrategyForAttr()这一步。每个AudioTrack不管你有没有显式指定设备都会先根据usage算出一个策略标识routing strategy比如STRATEGY_MEDIA、STRATEGY_SONIFICATION、STRATEGY_PHONE、STRATEGY_DTMF等。这个strategy决定了后面所有路由规则。换句话说你创建Track时填的AudioAttributes已经悄悄决定了你将来会被分配到哪一类设备池。很多应用开发者不明白为什么指定了设备不生效其中一大原因就是usage和device不匹配。比如你拿一个USAGE_MEDIA的Track指定DEVICE_OUT_EARPIECE听筒策略引擎按STRATEGY_MEDIA的规则算听筒根本不在媒体策略的候选设备列表里你的指定自然就被丢弃了。3. 路由策略引擎设备到底是怎么“算”出来的3.1 strategy与device_category听筒、扬声器、耳机、蓝牙的优先级Android从8.0开始把策略引擎独立到audio_policy_engine模块设备选择逻辑主要在Engine类的getDeviceForStrategy()中。不同策略对应不同设备候选集而且有明确的优先级顺序。这里我根据AOSP源码整理一份简化的策略设备优先级表不同厂商HAL可能微调但大逻辑基本一致路由策略典型场景候选设备优先级从高到低按设备连接状态过滤后STRATEGY_MEDIA音乐、视频播放蓝牙A2DP/BLE音频 - USB Device - 有线耳机/头戴 - 扬声器STRATEGY_SONIFICATION铃声、系统声音有线耳机/头戴 - 蓝牙A2DP - 扬声器铃声场景比较特殊可能同时走多路STRATEGY_DTMF拨号音通话音频路径 - 有线耳机 - 听筒/扬声器STRATEGY_PHONE语音通话听筒 - 有线耳机 - 蓝牙SCO - 扬声器STRATEGY_ENFORCED_AUDIBLE强制音频有线耳机 - 扬声器受强制使用配置限制源码里这个逻辑写得很直白getDeviceForStrategy()返回的不是一个具体设备而是一个audio_devices_t位掩码代表本策略当前“可用”且“最匹配”的设备集合。它内部会检查每个候选设备是否已连接isDeviceConnected、是否处于可用状态音量不为0、未被其他策略独占等。3.2 默认路由计算规则为什么你的指定会被“盖掉”重点来了getOutputForAttr()拿到你传入的selectedDeviceId后并不直接采用。它会先通过getDeviceForStrategy()算出一个策略默认设备然后做一次“期望设备 vs 策略设备”的匹配判断。源码里对应逻辑大致是这样的如果调用方指定了设备且这个设备在当前策略的候选设备列表里同时这个设备处于已连接状态那么getDeviceForStrategy()会优先返回你指定的设备否则返回策略默认设备。这个“如果”条件看着简单实际使用时踩坑点非常多。第一个坑指定设备时必须是AudioDeviceInfo对应的完整设备类型。有些开发者会把AudioDeviceInfo.getId()和一个audio_devices_t混淆传入的是一个port id而不是device type底层比较时压根匹配不上。第二个坑设备已连接但策略不可用。比如蓝牙耳机连上了但你连接的是HFP通话profile而不是A2DP那么在STRATEGY_MEDIA的候选设备列表里该设备不可用策略引擎照样会fallback到扬声器。你指定蓝牙耳机得到的还是扬声器。第三个坑多个AudioTrack抢占时的策略竞争。系统里永远不只你一个Track可能出现两个Track同时播放分别属于不同strategy的情况。比如导航语音用USAGE_ASSISTANCE_NAVIGATION_GUIDANCE音乐App用USAGE_MEDIA。当导航语音开始播放时策略引擎可能会根据声音事件切换到听筒或耳机通道等导航语音结束再切回来。这时你的setPreferredDevice会因为其他策略的临时介入而被临时“盖掉”。3.3 fast/direct路径绕开策略引擎的特殊通道Android的音频播放请求除了普通模式还有两种特殊路径fast mixer路径和direct output路径。这两条路径的设备选择逻辑和普通路径完全不同。fast路径对应AUDIO_FLAG_FAST主要给低延迟场景用比如游戏、实时K歌。当AudioTrack请求fast标志且采样率/通道数/format与当前fast mixer线程匹配时系统会把Track直接挂到已有的fast mixer线程上。而fast mixer线程是在启动时就绑定了一个固定的输出设备通常是primary output这个设备不会因为单个Track的setPreferredDevice而改变。这就是为什么低延迟模式下指定设备常常无效。direct路径对应AUDIO_FLAG_HW_AV_SYNC、AUDIO_FLAG_DIRECT、AUDIO_FLAG_COMPRESS_OFFLOAD等场景。这类Track通常在HAL层就有独立的输出流由真正的音频DSP/解码器直接处理。direct output匹配时系统会遍历所有direct输出profile查找支持对应采样率、格式、通道数和设备的输出。如果你指定的设备没有注册对应的direct profile创建直接输出就会失败或者fallback到普通路径设备自然也不是你指定的那个。我个人调试过的一个案例某款USB DAC在指定DEVICE_OUT_USB_DEVICE播放DSD时因为该DAC没有注册Direct PCM profile系统直接放弃了direct路径走普通混音输出结果DSD变成了PCM播放设备也“看起来像是”没有生效。实际上设备是生效的只是路径和预期完全不同。4. 设备变化时的路由更新机制4.1 设备插拔后AudioPolicyManager做了什么前面讲的主要是Track创建时的设备选择但设备选择还有另一半播放过程中设备发生变化比如插拔耳机、蓝牙断开、USB DAC插拔。这部分由AudioPolicyManager::setDeviceConnectionState()驱动。设备连接状态变化会触发一个完整的路由重算流程先更新系统的设备连接表记录哪些设备在线然后遍历所有策略调用getDeviceForStrategy()重新计算每个策略的最新设备再遍历所有活动的PlaybackThread看当前线程绑定的设备是否需要切换。如果发现某个播放线程的设备已经不匹配AudioPolicyManager会调用AudioFlinger的setOutputDevice()来切换输出。这里有个值得注意的点切换设备不一定会中断播放。如果新旧设备在同一个PlaybackThread的scenario内系统可能直接调整HAL参数让音频流无缝切到新设备如果跨越了线程比如从speaker切到A2DP需要新的A2DP输出线程AudioFlinger会做一次“冷切换”表现上就是播放断了几十毫秒。很多播放器App遇到的“拔出耳机后声音还在继续”或“蓝牙断开后播放直接STOP”就是这个切换过程引发的问题。具体表现为设备拔掉后策略引擎把strategy设备改成speaker但原来的Track可能因为AudioTrack内部状态机判断“输出不可用”自动进入paused/stopped状态。4.2 客户端如何感知路由变化两个回调用途不同对应用层来说设备变化有两种监听方式容易混淆。第一种是AudioManager.registerAudioDeviceCallback()它监听的是系统音频设备列表的变化即插拔事件本身。回调里会给你一个AudioDeviceInfo[]数组你可以从中获取当前所有已连接设备。这个接口适合做UI展示、设备列表刷新它不关心你的Track是否正在使用这些设备。第二种是AudioTrack.addOnRoutingChangedListener()它监听的是你的Track的实际路由变化。每当系统策略引擎把你的Track的输出设备切换时回调会触发。这个接口才是判断“我的播放是否被切走了”的正确途径。源码里AudioTrack.addOnRoutingChangedListener()注册后native层会设置一个routing callbackAudioFlinger在输出设备变化后通过IAudioTrack::onRoutingChanged()的Binder回调通知客户端。客户端收到回调后会重新获取当前路由信息判读最新的设备ID。我建议所有做播放器的开发者主界面监听AudioDeviceCallback用于展示播放引擎内部监听OnRoutingChangedListener用于处理播放状态。很多开发者只加了前者导致UI显示耳机已经断开但播放引擎不知道设备变了出现“插着耳机暂停拔了耳机还在暂停”之类的状态错乱。4.3 新版本API更高层的路由控制Android 12之后系统提供了AudioManager.setCommunicationDevice()和AudioManager.setPreferredDeviceForStrategy()接口允许应用不直接操作某个Track而是按strategy级别做路由控制。前者主要面向通话/通信类音频指定通话音的输入输出设备后者可以给某个strategy全局设置偏好设备相当于对整类音频生效。这些新接口内部依然走的是AudioPolicyManager的设备选择流程只是把“设备偏好”挂到了strategy上而不是单个Track上。对开发者的意义是你可以更粗粒度地控制路由而不用追踪每个Track的生命周期。但副作用也很明显——如果多个App同时设置了不同偏好后设置的可能覆盖先设置的系统里并没有一套公开的“偏好冲突仲裁”机制结果基本靠后写覆盖。5. 高频问题排查实录设备选择不生效的几种典型情况5.1 一套快速定位的方法论遇到设备选择不生效我建议按这个顺序排查不要一上来就改代码。首先用adb shell dumpsys media.audio_policy看当前策略引擎的设备状态和路由表确认目标设备是否真的处于已连接状态、是否进了候选列表。然后用adb shell dumpsys media.audio_flinger看Track实际挂在了哪个PlaybackThread上确认最终输出句柄。如果前两步还不能定位就在源码层加日志logcat -v threadtime过滤AudioPolicyManager、getOutputForAttr、getDeviceForStrategy这三个关键位置看清楚策略引擎最终返回的设备ID和你预期的差异。这一步能直接告诉你到底是指定被丢弃了还是指定根本就没传到底层。5.2 三个高频Case复盘Case 1TWS耳机连接后媒体声音不从耳机出。这类问题多发生在蓝牙耳机只连接了HFP/HSP没有建立A2DP流的时候。策略引擎在STRATEGY_MEDIA的候选设备列表里找不到DEVICE_OUT_BLUETOOTH_A2DP自然fallback到speaker。解决方式是使用BluetoothA2dp主动请求连接A2DP profile或者在App里检测到当前蓝牙只有SCO设备时提示用户到蓝牙设置里手动连接“媒体音频”。Case 2指定了USB声卡声音仍在扬声器。USB声卡在Android里的路由比较特殊。很多USB DAC需要系统注册对应的mixPort并声明支持direct输出。如果你只是setPreferredDevice而底层没有匹配的output profile系统会静默fallback。处理办法是确保USB DAC的profile在audio_policy_configuration.xml里配置了且声卡的输出采样率和format被策略引擎认可。还有一点部分设备只有插拔瞬间会重新扫描路由插上之后才创建Track可能匹配不到新设备需要在onAudioDeviceAdded回调后再设置设备。Case 3导航语音播放时音乐被“挤”到扬声器或听筒。这个其实是策略引擎的正常行为STRATEGY_SONIFICATION播放会临时抢占并改变策略切换向量。尤其是AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE这类低延迟提示音在设计上优先保证可听性路由到电话通道或听筒是预期行为。作为音乐App你改变不了这个但可以通过监听OnRoutingChangedListener感知变化等导航音结束后重新setPreferredDevice恢复自己的设备偏好。5.3 一个底层调试技巧用systrace抓Track创建时延如果怀疑设备选择耗时过长导致播放建链慢可以用systrace来分析。在Track创建前加一个trace点setPreferredDevice是一个createTrack_l是一个然后看AudioFlinger侧createTrack到getOutputForAttr返回的耗时。正常来说如果设备已在候选列表这段耗时应该在微秒级如果出现几十毫秒的耗时多半是策略引擎在枚举direct output profile或者HAL层在做设备探测。这个对做低延迟场景优化特别有用。6. 留给自己和后来人的一些笔记AudioTrack设备选择这条链路最大的认知陷阱就是“指定设备强制设备”。基于源码我们可以确认setPreferredDevice设置的deviceId最终传给getOutputForAttr只是策略引擎的一个参考输入。getDeviceForStrategy才是真正决定设备的函数它根据strategy、策略优先级、设备连接状态、设备可用性来综合判断。fast/direct路径有自己独立的设备绑定逻辑不一定走主策略。设备插拔后策略引擎会重算所有strategy的设备影响的是所有活动Track。如果做Framework定制想真正改变设备选择行为比起在App层堆代码更推荐直接修改策略引擎的设备优先级映射表或者修改getDeviceForStrategy里的候选设备判断逻辑。这个方向是所有设备路由问题的总闸门改一层所有App都跟着变。最后分享一个笨但可靠的学习路径在任何Android源码版本上从AudioPolicyManager::getOutputForAttr开始打断点到getDeviceForStrategy再看Engine类的具体实现跟着一条Track从创建到播放完整走一遍比读十篇源码分析文章都有效。设备选择这件事纸上谈兵没意义亲自跑一遍就全通了。