
这个系列写到第五篇前面几篇我们把音频系统的骨架——AudioFlinger、HAL、AudioTrack/AudioRecord通路——基本摸了一遍。写第一篇时我就说过Android音频最劝退新人的地方不是AOSP代码量而是“东西太多不知道从哪条线串起来”。今天这篇就是把那条线里最关键的一个决策中枢单独拎出来讲透AudioPolicyService的策略管理。它到底负责什么说人话就是一句话你的App播放一个声音时用什么设备播、多大音量、要不要打断别人、来电时它该不该闪避这四件事全是它拍板。这些看似零散的问题在代码里被抽象成“策略Policy”。所以标题里的“策略管理”本质是研究AudioPolicyService怎么根据使用场景、设备状态、音量映射做出一系列决策。这篇适合正在看framework层代码、做ROM定制、搞车机/电视音频适配的兄弟们。如果你是刚入门我会尽量把决策链路拆开揉碎从一次播放的完整旅程讲起保证你跟着走完一遍回去再看AOSP里的audio_policy代码会有一种“原来就这”的通透感。需要先说清楚一点虽然标题挂着Android 15但AudioPolicyService的主体框架从Android 8引入AudioPolicyEngine、Android 11引入ProductStrategy之后已经基本定型。Android 15不是一次推翻式重构更多是行为细节的渐进式调整与加固。下文我会在相关章节里标注哪些是Android 15前后值得关注的变化点方便你们对照自己的代码版本。1. 先搞清楚AudioPolicyService在整个音频系统里的位置1.1 从一次“按下播放键”的旅程说起你打开音乐App点了一下播放这背后发生了什么用最粗的粒度讲App通过AudioTrack把音频数据写进AudioFlinger管理的共享内存AudioFlinger里的MixerThread负责混音混完之后把数据交给Audio HALHAL再写进声卡驱动。这条链路听起来很顺畅但有一个关键问题被跳过了App播放时系统怎么知道要把声音送到扬声器、耳机还是蓝牙答案就是AudioPolicyService。AudioTrack在创建的时候会通过AudioFlinger向AudioPolicyService发起一次getOutputForAttr查询。这个查询带着App的使用场景比如媒体、导航、通话、音频属性采样率、声道数、格式还带着当前系统的设备状态。AudioPolicyService根据这些输入返回一个“输出”的句柄告诉AudioFlinger这条流应该往哪个声卡、哪个端口写。如果用一个比喻AudioFlinger是酒店前台负责安排房间里的事务AudioPolicyService是大堂经理他知道今天哪些房间能住人、哪个客人是什么身份、该给什么待遇。你按下播放键的瞬间真正替你决定“住哪间房”的不是前台而是大堂经理。1.2 策略服务管的四件事把AudioPolicyService的职责拆开其实就四件事路由策略根据使用场景、设备可用状态、强制设备偏好决定输出到哪个设备。这是最核心、也最容易出问题的一块。音量策略管理各种流类型的音量索引把音量的“格数”换算成具体衰减dB值并针对不同设备类别给出不同曲线。并发与焦点策略协调多个App同时播放时的行为比如导航播报时压低音乐音量、来电时暂停其他播放。这就是AudioFocus机制。动态策略与设备连接处理耳机插拔、蓝牙连接、USB声卡插入等动态事件同时提供运行时增加/修改路由策略的接口。这四件事在代码里分别对应AudioPolicyInterface接口族中的getOutputForAttr、setStreamVolume/getStreamVolume、requestAudioFocus、setDeviceConnectionState等关键方法。掌握了这四块你基本就掌握了AudioPolicyService的全部核心逻辑。1.3 Android 15这个节点策略层有什么变化很多兄弟一看到“Android 15”就以为策略层又大改了一版其实没有。Android 15在audio_policy层面真正值得关注的变化有这几个第一个是AudioFocus的默认行为更“懂得谦让”了。对targetSdk 35的应用焦点丢失时的淡化duck处理更加平滑系统会尽量避免突然一击的停顿感。第二个是通信设备路由communication device选择逻辑在持续完善setCommunicationDevice相关API成为语音类应用的推荐做法。第三个是底层被要求适配16KB page size这意味着所有native库的链接和对齐方式都有调整HAL厂商如果还抱着老构建不放在Android 15的三方兼容性测试上会很痛苦。这些变化属于“底层加固、上层行为微调”AudioPolicyManager的职责边界和决策流程本身没有翻天覆地。真正推翻式的重构是Android 8引入AudioPolicyEngine、Android 11引入ProductStrategy那两次。所以读这篇文章的时候你手里的代码不管是Android 12还是Android 15核心思路都能对上只是部分API名和配置项有差异。2. 策略管理的心脏AudioPolicyManager与AudioPolicyEngine2.1 AudioPolicyManager策略决策的“程序”AudioPolicyManager是整个策略服务的实现核心。它不是一个独立进程而是跑在audioserver进程里的一个核心对象。audioserver启动时会创建AudioPolicyServiceAudioPolicyService的onFirstRef里会实例化AudioPolicyManager由后者读取配置文件、初始化引擎、注册各种回调。从类关系上看AudioPolicyService是binder服务的外壳负责IPC通信AudioPolicyManager才是真正干活的类。它继承自AudioPolicyInterface实现了几百个方法但真正的策略决策逻辑并不全在它身上——很多规则细节被下沉到了AudioPolicyEngine。这里要提一个重要变化Android 8之前策略规则是写死在AudioPolicyManager代码里的各种if-else判断“如果是有线耳机就优先耳机、如果是媒体流就选A2DP”之类的逻辑。那时候做ROM定制的同学都有体会想调整设备优先级得改C代码然后重新编译system镜像成本极高。所以Android 8开始AOSP把“规则”从“程序”里抽了出来变成了可配置的XML这就是AudioPolicyEngine的由来。2.2 AudioPolicyEngine把决策规则从代码中抽出来AudioPolicyEngine不是一个独立进程也不处理音频数据它只做一件事根据输入的“条件”用配置好的“规则”算出输出。这些规则定义在/vendor/etc/audio_policy_engine_configuration.xml或/system/etc这个文件里。配置文件里几个核心概念先理清Selector选择器引擎的入口条件比如输入的使用场景usage、音频流类型stream type、设备连接状态。ProductStrategy产品策略一组使用场景的集合比如STRATEGY_MEDIA聚合了USAGE_MEDIA、USAGE_GAME等。引擎根据Selector命中某个ProductStrategy再根据该策略去选设备。VolumeGroup音量组将音频流按策略收入不同的音量组一组共用一条音量曲线。VolumeCurve音量曲线定义音量索引从0到max时对应的衰减dB值可以按设备类别device category区分。一个简化版的engine配置长这样audioPolicyEngine productStrategies productStrategy nameSTRATEGY_MEDIA attributes usage nameUSAGE_MEDIA/ usage nameUSAGE_GAME/ /attributes deviceCategories deviceCategory nameDEVICE_CATEGORY_SPEAKER/ deviceCategory nameDEVICE_CATEGORY_HEADSET/ /deviceCategories volumeGroup nameMEDIA_VOLUME_GROUP/ /productStrategy /productStrategies /audioPolicyEngine实际AOSP里的配置远比这个复杂但核心就是这个结构什么场景进什么策略策略对应哪些设备类别设备类别又挂哪个音量组。你改配置的时候本质上就是在改这张“映射表”。2.3 engine与manager的协作流程以选设备为例那AudioPolicyManager和AudioPolicyEngine到底怎么配合我用getDeviceForStrategy这个最经典的流程走一遍AudioPolicyManager先从调用方拿到音频属性AudioAttributes内部转成usage、stream type等信息。manager先检查是否存在“强制设备”比如setPreferredDevice设了偏好设备或者forceUse配置了强制场景如果有直接返回强制设备这是最高优先级。如果没强制设备manager调用engine的接口传入product strategy和当前设备状态engine根据配置算出候选设备列表。manager拿到候选设备列表后还要和当前可用的设备集合求交集同时根据调用方的flags比如是否是offload、是否是direct输出做过滤。最后落到一个具体的device descriptor上再返回对应的输出。这个流程可以用一段伪代码表示audio_devices_t AudioPolicyManager::getDeviceForProductStrategy( product_strategy_t strategy, bool fromCache) { // 1. 强制设备优先级最高 if (mForcedDeviceForStrategy[strategy] ! AUDIO_DEVICE_NONE) { return mForcedDeviceForStrategy[strategy]; } // 2. 交给engine算规则结果 auto devices mEngine-getDevicesForStrategy(strategy); // 3. 和当前可用设备求交 devices getAvailableDevicesMask(); // 4. 处理flag过滤比如offload/direct devices filterDevicesByFlags(devices, ...); // 5. 候选非空则选第一个 if (devices ! AUDIO_DEVICE_NONE) { return popCount(devices) 1 ? selectBestDevice(devices) : devices; } // 保底primary输出 return AUDIO_DEVICE_OUT_SPEAKER; }这个过程里最值得玩味的是第4步。filterDevicesByFlags看起来只是过滤flag但它实际上决定了你有没有资格使用低延迟、offload、直通这些高级通路。很多“声音能出来但音质不对”或者“延迟贼高”的反馈根源就在这一步把该用的通路过滤掉了。2.4 配置加载失败的坑改配置文件踩过的坑值得先提前说一嘴AudioPolicyManager构造时会加载所有XML配置一旦解析失败整个audioserver会反复重启设备直接没有声音。最麻烦的是有时候语法没问题而是设备端口名对不上——engine配置里的设备名和audio_policy_configuration.xml里的设备端口名不匹配同样会导致加载失败。我的经验是改完XML先做语法校验再检查名称一致性最后才放到设备上。即便是开发机也要做好“变砖”的心理准备最好有串口或adb能及时捞日志。3. 路由策略的完整链路一个AudioTrack是怎么被指派设备的3.1 从Usage到ProductStrategy场景怎么变成策略路由决策的第一步是理解“场景”。App在创建AudioTrack的时候会带上AudioAttributes里面最重要的字段是usage和contentType。usage描述的是“我要做什么”比如USAGE_MEDIA、USAGE_ASSISTANCE_NAVIGATION_GUIDANCE、USAGE_VOICE_COMMUNICATIONcontentType描述的是“内容是什么”比如音乐、语音、声效。到了native层AudioPolicyManager会把这个usage映射到一个整数枚举的product strategy。Android 11之前叫strategy之后叫product strategy本质没有变化只是语义更丰富了。拿一份典型映射表来说usageproduct strategyUSAGE_MEDIA / USAGE_GAME / USAGE_UNKNOWNSTRATEGY_MEDIAUSAGE_ASSISTANT / USAGE_ASSISTANCE_NAVIGATION_GUIDANCESTRATEGY_ENFORCED_NAVIGATION 或 STRATEGY_ASSISTANTUSAGE_VOICE_COMMUNICATION / USAGE_CALL_ASSISTANTSTRATEGY_VOICE_COMMUNICATIONUSAGE_NOTIFICATION / USAGE_NOTIFICATION_RINGTONESTRATEGY_SONIFICATIONUSAGE_ALARMSTRATEGY_ALARM为什么Android 11要做这个升级因为老的strategy和stream type耦合太紧而stream type只有11个很多新场景表达不了。比如“导航播报的时候音乐要闪避”老方案里你得单独判断usage是不是导航否则它和媒体共用一条策略根本没法差异化处理有了product strategy每个场景集合可以独立配置设备类别、音量组、闪避规则定制空间一下就打开了。3.2 AudioPolicyService::getOutputForAttr里面发生了什么从AudioFlinger往AudioPolicyService发的路由查询核心入口是getOutputForAttr。我简化一下它的调用链AudioFlinger::createTrack - AudioPolicyService::getOutputForAttr - AudioPolicyManager::getOutputForAttr - getDeviceForProductStrategy - checkOutputForAttributes / selectOutput这里有个和旧版本不同的地方Android 12之后路由选择开始区分“在旧输出上重定向”和“打开新输出”两种。如果当前输出流mixer thread的属性可以复用manager会直接返回现有输出而不是重新开一条。这个决策对资源开销影响很大因为每条输出都对应一个独立的混音线程。getDeviceForProductStrategy内部查找设备时会把device descriptor设备描述符拿来做综合打分。评分维度包括设备类型、是否支持采样率转换、是否是位完美bit-perfect路径、是否支持offload等。说人话就是系统最终选的设备不一定是“硬编码优先级最高”的而是“在当前条件下最划算”的。我在Android 14上调试过一个案例USB DAC插入后媒体播放没有自动切到USB而是继续留在扬声器。日志里看起来路由逻辑正常候选设备里也有USB但最终分数被“采样率不支持44.1kHz原样输出”给扣掉了因为USB DAC的profile只声明了48kHz的PCM能力。这种问题不做完整链路追踪光看路由结果根本定位不到。3.3 设备插拔时的动态重路由路由决策不只是创建Track时做一次。你戴着耳机听歌突然把耳机拔了声音要在几百毫秒内切到扬声器反过来你正在外放突然连上蓝牙耳机系统得决定要不要立刻切过去。这些都是AudioPolicyService的“动态重路由”机制在起作用。触发入口是setDeviceConnectionState。它干的事情分三步更新设备连接状态把它加入或移出可用设备集合然后遍历所有正在活动的client检查当前路由在新设备状态下是否还是最优如果不是最优调用AudioFlinger的restoreOutputRouting或线程重路由把已经打开的流切到新设备上。关键一点切换不是无条件的。举个例子如果你正在打电话蓝牙耳机连上系统会立刻切到蓝牙但如果只是放音乐大多数策略实现里即使蓝牙连上也不会打断正在播放的扬声器音乐这是为了避免频繁切换带来的体验割裂。不同厂商在这个“切换阈值”上的调节力度差异很大有的人喜欢即时切换有的人喜欢等到下次播放再切。这块没有绝对对错完全是策略取舍。3.4 输出profile与端口匹配的细节很多做HAL的兄弟容易忽略一件事路由选择的不仅是“设备”还有“输出端口组合”。同一个USB声卡驱动里可能声明了多个profile一个支持PCM 16bit 44.1/48kHz另一个支持DSD。AudioPolicyManager在拿到候选device之后会遍历这台设备的所有profile寻找和调用方属性最匹配的那一个。profile匹配失败的常见表现是声音出来了但HAL层一直在做重采样或者干脆匹配不到任何profile只能退回到primary output的默认路径。日志里通常会看到类似getOutputForAttr: no output available之类的字眼但往往不会告诉你为什么profile匹配失败。调试这类问题的建议是先确认配置文件里声明的采样率、通道数、格式是不是和HAL驱动真实能力一致特别是第三方声卡文档参数和实际能力跑偏是常事。然后检查AudioFlinger里那条mixer thread的采样率是不是被某些高优先级流锁定了。我处理过一个诡异case一个App播放192kHz的流把mixer线程采样率锁上去后所有其他流全部被迫重采样音质劣化明显根源就是profile匹配时忽略了mixer线程的已有设定。4. 音量策略VolumeGroup与VolumeCurve的配置化设计4.1 音量不再是“流类型一个数字”Android早期的音量模型很简单每个stream type一个音量索引系统提供一个0到15或0到7的滑块然后按一个固定的衰减公式换算成dB。这个模型有两个天生缺陷第一音量调节粒度和用户体验严重脱节很多用户反映“低一格太轻高一格太响”第二不同设备类别耳机、扬声器、蓝牙的听觉灵敏度差异很大同一索引值在不同设备上听感完全不同。VolumeGroup的出现就是为了解决这两个问题。它不再以一个stream type为单位管理音量而是以“策略”为单位把多个stream type聚合到一个组里共用一条音量曲线。举个例子STRATEGY_MEDIA对应的音量组可能叫MEDIA_VOLUME_GROUP它同时收纳了STREAM_MUSIC、STREAM_GAME、STREAM_SYSTEM等多个stream type。媒体音量调节时这些流一起跟着变从用户视角看就是“媒体音量”。更重要的是音量曲线现在可以按设备类别分开配置。同样是媒体音量接耳机时用一条高灵敏度曲线戴头戴耳机时用另一条外放用第三条。这在以前是不可能的。4.2 VolumeCurve曲线的定义与换算VolumeCurve在engine配置文件里长这样volumeGroup nameMEDIA_VOLUME_GROUP volumeCurve volumePoint volume index0 attenuation-1000/ !-- 静音 -- volume index10 attenuation-560/ !-- 最小可听 -- volume index50 attenuation-200/ volume index100 attenuation0/ !-- 最大 -- /volumePoint /volumeCurve /volumeGroup这里的attenuation单位是毫dBmdB-1000表示静音0表示最大增益。系统在计算实际音量时先找到当前音量索引对应的两个point然后做线性插值得到最终衰减值再叠加到流增益上。注意这里说的是线性插值但人耳对音量的感知是对数级的。所以配置曲线时如果想做出“听起来是均匀变化”的效果度数的增减应该尽量维持感知一致性插值出来的点往往不是直线而是一条下凹或上凸的曲线。很多厂商第一次调参时会疑惑“为什么我配了直线音量还是前段变化大后段变化小”因为你给的是mdB对数域感知本身就是非线性的。4.3 定制音量策略时最容易踩的坑做音量策略定制我总结了几条血泪经验第一个坑只改stream音量映射没确认VolumeGroup归属。很多App间的音量互相干扰其实是stream type被映射到了错误的volume group里。比如把STREAM_SYSTEM错误地归到了闹钟组导致系统音量和闹钟音量绑在一起。第二个坑曲线点太少。只有一个静音点和最大点中间全靠插值出来的结果往往会让低音量段衰减过快。建议至少配置5个以上的point尤其是音量索引30以内的区间那里的听感变化最敏感。第三个坑设备类别配置过粗。engine配置里默认的device category往往只有speaker和headset如果你只改了speaker曲线的最大音量耳机、蓝牙、USB全部受影响。我建议至少拆成SPEAKER、WIRED_HEADSET、BT_A2DP、USB_DEVICE四类分别调。第四个坑改完XML忘记清数据和重启。音量配置在服务启动时读入内存不是每次都重新读文件。改完配置不重启audioserver的话你会看到dumpsys里还是旧曲线然后开始怀疑人生。这条经验同样适合Android 15系统对音量配置的加载时机没变改完配置必须adb shell stop adb shell start或者直接重启。5. 动态策略运行时调整策略的那些路子5.1 DynamicPolicy到底能干什么有些场景比如录屏直播、正在通话时要采集另一侧的声音这种需求没法在启动配置里预判因为触发时机完全由App决定。这时候就需要动态策略接口了。AudioPolicyService实现了一个IDynamicPolicy接口典型方法包括registerPolicyMixes、unregisterPolicyMixes等。动态策略的核心概念是AudioMix音频混音规则。一个mix包含三类关键信息允许或排除哪些usage、允许哪些设备、路由方式是强制还是建议。通过注册一个mixApp可以请求系统把某个usage的音频流路由到一个特定的输出或者从某个输入源采集声音。最常见的场景就是录屏App注册一个REMOTE_SUBMIX类型的mix之后系统把内录音频和麦克风采集混好交给应用这就是录屏“录制内部声音”功能的底层实现。5.2 AudioMix与并发场景的管理动态mix最容易被低估的是它和现有路由策略的交互。系统里同时存在静态策略和动态mix时AudioPolicyManager会做一个“合并决策”如果动态mix匹配了正在播放的usage会优先按动态mix的路由来但音量、焦点等策略逻辑仍然走原有框架。这带来一个很有意思的问题动态mix不应该无限制地拦截音频流。一个设计良好的动态mix会声明明确的usage白名单和路由规则而不是“我全要”。我见过有厂商在录屏功能里把mix写成“capture all”结果所有App的声音都往录屏通道走用户正常外放直接没声音。定位了三天最后发现是动态mix的usage过滤配置漏写了。这告诉我们动态策略用得好是神器用不好是事故源头。5.3 焦点也是策略的一部分音频焦点AudioFocus虽然名义上归AudioManager管但它对策略的影响极大我会把它看作策略管理的一部分。每个App要开始“重要的”声音播放之前应该先申请焦点系统根据焦点持有者的优先级决定是让新的播放开始、打断旧的播放还是让旧的播放压低音量duck。Android 15对焦点机制做了一些体验层的改进对target 35的应用焦点丢失时的淡化处理更平滑系统默认帮你做渐变衰减而不是瞬间“啪”一下断开。开发者在适配的时候需要注意申请焦点时正确设置AudioFocusRequest的setAcceptsDelayedFocus和setWillPauseWhenDucked否则你的App在焦点竞争场景下会表现出非常僵硬的行为。对做策略定制的人来说焦点状态是路由和音量决策的重要输入变量。比如导航播报时音乐音量下滑3dB以上这个动作就是AudioPolicyService在焦点事件里做出的音量调整。理解了焦点机制你才真正理解了为什么有些播放行为看起来像“被策略管住了”。6. 常见问题与排查技巧实录6.1 播放没有声音或者声音从错误的设备出来这类问题排第一几乎每天都有开发者来问。我的排查习惯是分四步走第一看路由结果。执行adb shell dumpsys audio重点看Outputs和Routes两个字段确认当前流被路由到了哪个设备。如果路由结果是NONE或者回退到了默认设备那问题就在策略层。第二看设备状态。执行adb shell dumpsys media.audio_policy确认耳机、蓝牙等设备是否在available devices列表里。设备不在列表里后面全是白搭。第三看决策过程。logcat里过滤AudioPolicyManager和AudioPolicyEngine找到getOutputForAttr附近的关键日志看候选设备列表和flags过滤情况。第四确认HAL层接收到的设备ID。如果HAL层收到的输出设备ID完全不对那可能是策略配置文件和HAL上报的device端口对不上。现象优先排查方向路由到默认设备检查strategy的设备优先级和可用设备集合路由到NONEflags过滤或profile匹配失败设备在但切不过去检查动态重路由阈值和mix策略时好时坏怀疑设备连接状态上报不稳定6.2 音量条拖着没反应或低音量直接没声音量问题里最典型的有两类音量条有反应但实际音量不变音量调低一两格就完全静音。前者最常见的原因是stream type被映射到了一个没有VolumeCurve的volume group里。系统操作音量条修改的是stream volume但如果它对应的volume group没有配置曲线最终增益换算就会落到错误路径上。后者基本是曲线设计问题。前面说过插值在低音量段如果衰减过陡会导致音量索引稍微调低就触底。排查时先看dumpsys audio里的volume group配置确认当前stream的索引绑定的曲线point分布是否合理。还有一个容易忽略的点masterMute。有些设备在启动时因为音频策略初始化失败会置一个全局静音表现是“无论怎么调音量都没声音”。日志里搜setMasterMute能复现的话大概率就是策略加载阶段出了问题。6.3 策略配置改崩了audioserver反复重启怎么办修改audio_policy_configuration.xml或audio_policy_engine_configuration.xml后最容易碰到的硬故障就是audioserver反复重启。这时候设备通常没有声音因为音频整个服务没起来。第一步是捞日志。adb logcat -b crash和adb shell dmesg都看一眼重点是找到AudioPolicyManager初始化失败的堆栈。第二步是核对配置。常见问题有几类XML语法错误、设备port名称拼写不一致、引用了不存在的usage、volume curve的point越界。我的习惯是做一个“最小化改动”测试先只改一个字段验证没问题再继续避免一次性引入多个变量。这里强烈建议配置文件和数据目录分开管理线上回归时方便快速回滚。6.4 适配Android 15的几个建议最后聊聊Android 15适配的实操建议。对于HAL厂商第一优先确认16KB page size下so库的对齐没问题否则挑设备概率极大。第二检查AIDL HAL接口里枚举值和标志位的对齐Android 15清理了不少隐藏的type转换bug如果你的实现用了隐式转换编译期可能没问题运行期就说不准了。对于framework定制方Android 15对焦点行为的调整意味着所有播放器应用都需要重新测试一遍焦点场景。不要默认老代码能无缝迁移建议把“来电打断”“导航闪避”“多App并发播放”这几个场景全部回归一遍。对于做产品策略的同学我的建议是“少改多验证”。产品策略的调整动一发牵全身一个strategy变动会影响路由、音量、焦点三套机制。每次改动先在模拟器上跑通基础场景再上真机做完整音频回归。最后再分享一个小技巧调试AudioPolicyService时dumpsys media.audio_policy里的信息比dumpsys audio更细很多厂商log里没有的东西都在这里有。遇到疑难杂症先把这两个dump完整拉下来对照着看往往能找到突破点。我自己做车机项目两年最深的体会就是音频策略这件事配置远比代码更容易成为瓶颈而排查配置问题靠的从来不是灵感而是一步一步把链路走完的耐心。