ARTICLE DETAIL

资讯详情

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

Android音频核心AudioPolicyService:从路由决策到音量计算源码剖析

Android音频核心AudioPolicyService:从路由决策到音量计算源码剖析 干了这么多年Android框架要说最容易被App层开发忽略、又最能解释为什么声音从奇怪的地方冒出来的模块AudioPolicyService绝对排前三。平时大家用AudioManager接口调音量、切播放设备觉得理所当然一旦遇到插了耳机声音还从喇叭出、或者打电话时媒体音量诡异的场景就只能在网上翻帖碰运气。这些问题的根子基本都在AudioPolicyService这一层它决定了哪路音频走哪个设备、音量该衰多少、谁有资格抢到出声的权利。这篇文章按源码解读的方式把AudioPolicyService从启动、配置加载、路由决策到音量计算这条主线拆开讲一遍。适合已经写过一些Android音频业务、或者正在啃framework源码的同学纯做UI层开发也不怕我会尽量把每个环节的为什么讲透让后面你排查音频问题时有据可循。1. 先把AudioPolicyService放到整条音频链路上看1.1 它到底是干什么的Android音频架构里最常被提起的是AudioFlinger它是音频数据的搬运工和混音器负责跟HAL层打交道把App的音频数据变成PCM输送给声卡。但搬什么、搬去哪、搬的时候音量多大这件事AudioFlinger自己说了不算决策权在AudioPolicyService手里。用生活化的方式理解AudioFlinger像一个音响师傅AudioPolicyService是站在旁边的小调度员。调度员怎么指挥完全看手里的规则手册——这本手册就是audio_policy_configuration.xml和产品策略配置。某App开始播放音乐时调度员要根据当前插着什么设备、系统处于什么音频模式正常、通话、会议、这个stream属于什么策略来决定让AudioFlinger输出到扬声器、耳机还是蓝牙同时还要算好该衰减多少dB别突然把鼓点砸到人耳朵上。从代码层级看AudioPolicyService是一个Binder服务继承自BnAudioPolicyService客户端通过IAudioPolicyService接口跨进程调用。服务端内部真正干活的是一套策略管理器Android 8.0之后把策略部分抽象成了AudioPolicyManagerInterface默认实现依托Engine引擎来做具体的选路判断。文中源码以Android 10/11为主低版本逻辑大体相似个别接口有差异我在关键地方会标注。1.2 需要的前置知识读懂AudioPolicyService最好先对三样东西有概念C和Binder基础至少要知道客户端调一个native方法最终怎么进到服务端AudioFlinger的基本职责知道混音线程、输出流是什么HAL层audio HAL的结构理解输出流的打开、设备切换最终落在哪。源码位置主要在frameworks/av/services/audiopolicy。这个目录里有managers、enginedefault、common、service几块分别对应管理逻辑、默认策略引擎、公共定义和Binder服务壳。AudioFlinger在frameworks/av/services/audioflinger。平时分析问题时两边日志要一起看很多路由bug是策略层以为自己切了执行层没切这种错位。2. 服务启动与配置解析一份XML如何变成一堆对象2.1 从onFirstRef说起AudioPolicyService的启动逻辑可以追溯到AudioPolicyService::onFirstRef()。在Android 10里核心代码大约长这样void AudioPolicyService::onFirstRef() { { AutoMutex lock(mLock); // 把AudiopolicyClient创建出来后续回调AudioFlinger用 mAudioPolicyClient new AudioPolicyClient(this); mAudioPolicyManager createAudioPolicyManager(mAudioPolicyClient); if (mAudioPolicyManager nullptr) { ALOGE(Failed to create audio policy manager); return; } } // 加载掉电状态、dump相关等 }很多人第一次看这段会困惑createAudioPolicyManager到底做了什么。它根据编译时选的引擎返回一个AudioPolicyManager实例构造函数里连着做了几件关键事读取系统属性比如ro.audio.*系列加载audio_policy_configuration.xml再初始化引擎。任何一个环节挂了整个音频服务都起不来所以配置文件的格式错误会直接导致系统声音功能瘫痪这个我们放到后面排查章节细说。低版本Android在同一个函数里还会创建AudioPolicyService::AudioPolicyClient的子类来代理AudioFlinger的调用高版本把这部分职责统一收进AudioPolicyClientInterface名字有变但意图一直没变——策略层和执行层必须解耦策略层调一个接口具体是找AudioFlinger还是找HAL由client实现去完成。2.2 配置文件里到底写了什么audio_policy_configuration.xml是策略服务的灵魂。一般在/vendor/etc/或/system/etc/下它定义了几类信息globalConfiguration全局配置比如是否支持speaker diversity、是否开启offloadmodules每个audio HAL模块常见的有primary、a2dp、usb、r_submixattachedDevices开机默认连接的设备outputs和inputs每个模块的输出/输入配置里面包含mixPort、devicePort、route和profile。mixPort可以理解成模块与上层mixer之间的桥是AudioFlinger实际打开的流devicePort是具体物理设备比如speaker、headphone、bluetooth_a2dpprofile列出了支持的采样率、格式、通道数。解析这段XML的代码在AudioPolicyConfig::load()它读出一个HwModule的集合然后创建AudioOutputDescriptor这些描述符对象。这里有个高频坑很多人想加一个新设备只改了devicePort的描述却没注意mixPort的profile是否声明了对应的采样率和格式。结果就是上层路由已经切过去了但AudioFlinger打开混音流时参数对不上要么打不开要么打开后出来的全是噪音。改配置文件前一定先把profile里的每个字段都核实一遍。2.3 Engine初始化与策略表的建立策略引擎是Android 8.0之后引入的抽象层。低版本时路由、音量策略都硬编码在AudioPolicyManager里厂商想改策略只能大改代码现在把这些策略数据化引擎根据product_strategies.xml、audio_policy_volumes.xml等配置建立策略表。初始化阶段引擎会做几件事解析所有product strategy把每个stream type映射到对应策略解析volume group把不同stream归组解析设备类型与增益曲线的对应关系。比如Music和Game属于MEDIA策略、来电铃声属于SONIFICATION策略、语音通话属于VOICE_CALL策略。策略的归属直接决定后续路由选择和音量曲线的查询路径这也是为什么你调节媒体音量时铃声也会跟着变——它们根本就在同一个策略组里。引擎初始化完之后AudioPolicyManager手里就有了三张关键表硬件模块表、输出/输入描述符表、设备状态表。后面所有路由和音量操作都是在动态更新和查询这几张表。3. 核心类关系与调用链3.1 从Binder到AudioPolicyManager的路径整个服务的调用链其实很清晰层级类职责Binder服务AudioPolicyService实现IAudioPolicyService接口做权限校验、参数校验策略管理AudioPolicyManager核心决策维护设备/输出/输入状态策略引擎Engine具体策略算法比如getDeviceForStrategy执行回调AudioPolicyClientInterface通知AudioFlinger/HAL执行切换客户端调用的典型入口比如AudioSystem::setDevicesConnectedState最终走到Binder服务再进入AudioPolicyManager。权限方面路由和设备连接这类操作要求MODIFY_AUDIO_ROUTING权限这个权限是signature级别的普通App拿不到调制音量走MODIFY_AUDIO_SETTINGS普通App经过用户授权也可以调用部分接口。写上层业务的时候经常能观察到一种现象用AudioManager.setMode()切模式没生效。这通常不是接口没调到而是AudioPolicyService内部做了模式状态机校验比如当前还有active的通信策略在占用新模式就会被打回。追这种问题就要顺着调用链看Binder服务端日志确认请求到底有没有进到AudioPolicyManager。3.2 核心描述符体系AudioPolicyManager内部维护的不是裸的设备号而是一堆描述符对象。输出侧有SwAudioOutputDescriptor和HwAudioOutputDescriptor它们描述一个输出流关联的mixPort、当前连接的devicePort、活跃的strategy计数、音量状态等。输入侧有AudioInputDescriptor管理录音流。描述符之间通过ID引用比如mOutputs是一个从audio_io_handle_t到描述符的mapmAvailableOutputDevices保存当前可用的输出设备集合。每次设备插拔或策略切换manager要同时维护多个集合的一致性假如只改了设备集合忘了更新输出描述符关联就会出现策略认为耳机在场、实际流没切过去的bug。这类问题在原生系统上不常见但在厂商深度定制、加了大量自研策略后成了重灾区。3.3 混音流的refCount机制还有一个特别有意思的机制是refCount。一个输出流可以被多个strategy同时使用比如音乐和通知都在MEDIA相关的流上播放。系统用strategyRefCount记录当前有多少策略正在使用这条流只有当计数归零时才允许关闭流。这就是为什么通知音播放完后音乐还能继续不会因为通知打断就把整个输出流关掉重开避免了音频断流和爆音。理解refCount对排查偶尔没声音很有帮助。如果某条log显示打开了流但refCount异常大概率是有上层App没有正确调用AudioSystem::stopOutput导致流一直占着没释放。4. 路由决策流程走读4.1 设备连接事件怎么触发路由设备插拔是路由重算最常见的触发源。系统检测到耳机插入后会通过JNI、AudioSystem一路调到AudioPolicyManager::setDeviceConnectionState。这个函数先判断设备类型再分派到不同处理路径void AudioPolicyManager::setDeviceConnectionState(audio_devices_t device, audio_policy_dev_state_t state, const char *device_address, const char *device_name) { // 输出设备走handleDeviceConnectionState // 输入设备走setInputDeviceConnectionState if (audio_is_output_device(device)) { handleDeviceConnectionState(device, state, device_address, device_name); } else if (audio_is_input_device(device)) { setInputDeviceConnectionState(...); } }handleDeviceConnectionState里会做几件事更新mAvailableOutputDevices把新设备关联到合适的输出流对每个受影响的strategy重新选路最后调用setOutputDevices真正执行切换。值得注意的是不是所有strategy都会被重算系统会按设备类型和strategy的关联程度决定哪些需要更新。比如插入蓝牙耳机主要影响MEDIA和SONIFICATION策略不会去动VOICE_CALL。4.2 getDeviceForStrategy怎么选路选路的核心函数是getDeviceForStrategy。在默认引擎里它会遍历当前策略下所有可用的product策略逐个判断候选设备是否可用、是否满足策略条件最后按优先级挑一个。低版本代码里硬编码逻辑很直白MEDIA策略下优先蓝牙A2DP然后是WIRED_HEADSET、USB最后才是SPEAKER。高版本改为由配置驱动product_strategies.xml里每个策略可以配置多个候选设备组合引擎按序匹配。这样厂商想调整USB声卡优先于蓝牙这类顺序改XML就行不用动代码。看这类代码有个窍门先关注availableOutputDevices再看策略的候选设备filter。大多数选路异常要么是可用设备集合不对要么是候选设备列表没包含新加的device。排查时用dumpsys看当前可用设备集合能省一大半时间。4.3 setOutputDevices到AudioFlinger的落地选完设备后setOutputDevices会把结果落到输出流上。它会对比目标设备集合和当前流实际设备集合有变化就走AudioPolicyClientInterface回调到AudioFlinger最终通过HAL去switch_device或者重建patch。Android 8.0之前这块用的是setOutputDevice加setParameters的组合之后慢慢过渡到AudioPatch机制。patch是一组endpoint连接关系AudioFlinger根据patch把混音线程输出转发到指定设备端点。打断这种patch图的关系比想象中容易出问题比如同时有多个patch指向同一个设备或者patch更新的时序不对都会导致切换后短暂无声。实际操作中我遇到过不少切换设备后第一次出声卡一下的案例很多就是patch重配期间混音线程还没有把新设备端点纳入处理加上上层App没做平滑处理造成的。系统侧能做的优化有限App侧尽量在切换后做几十毫秒的缓冲。这不是标准API能解决的属于经验性的规避手段。5. 音量策略与增益换算5.1 stream与音量的归属关系音量控制在AudioPolicyService里远比表面复杂。Android把stream type分成十几个类别但音量调节并不是每个stream单独一条线。归组之后调节一组里的任何一个stream其他stream跟着变。比如媒体音量调低了游戏和导航的音量也会相应变化因为它们都归在MEDIA策略下。理解这一点遇到为什么音乐音量变了导航没变这类疑问就不会懵。每个策略还有自己的音量范围配置。音频服务端用index0到100的整数档位作为用户层音量实际最终要换算成音频增益值。换算的依据是音量曲线定义在audio_policy_volumes.xml和VolumeCurves相关代码中。5.2 computeVolume里的dB换算过程AudioPolicyManager::computeVolume是整个音量控制的主逻辑。它做三件事查当前stream对应的音量曲线把用户index换算成attenuation衰减dB值结合当前输出设备类型做修正然后返回一个浮点增益给AudioFlinger。音量曲线本质上是一组离散点比如index衰减dB0-6050-241000中间值通过线性插值得到。扬声器和耳机的曲线不同原因是同一声压等级下耳机对人耳的实际响度感知更强直接用扬声器曲线会显得刺耳所以系统针对设备类型做了区分。这个细节很多定制ROM改砸过把耳机音量曲线直接套到扬声器上出来的声音要么闷要么炸。computeVolume最后还会叠加主音量级别的影响比如DND模式或者通话中压低媒体音量都是在这一步完成的。返回值不只是一路增益通常会拆成左右声道各自的值供AudioFlinger的追踪和混音线程使用。5.3 客户端设置音量的完整链路用一个实际场景走一遍全链路用户在系统设置里把媒体音量拖到70。AudioService.setStreamVolumeJava层做校验计算目标index调到AudioSystem.setStreamVolumeIndex通过Binder进入AudioPolicyServiceAudioPolicyManager.setStreamVolumeIndex检查索引范围更新对应stream的index对当前活跃的输出流调用computeVolume得到增益通过AudioPolicyClientInterface让AudioFlinger调用setStreamVolume混音线程的track增益随之更新。链路里每一步都有缓存和校验。比如index越界会直接截断stream处于不活跃状态时不会立刻推增益给AudioFlinger等流激活时才应用。这也是为什么设置里音量看着变了但播放时音量和之前一样——因为新音量还没有被应用到还未激活的流上必须等下次播放时才生效。遇到这类问题别急着怀疑代码先确认流的状态和时机。6. 排查实战与避坑清单6.1 常见问题速查我把平时遇到的高频问题整理成一张速查表方便大家按图索骥现象可能原因排查切入点插耳机后外放不停路由策略没匹配到耳机查看可用设备集合、getDeviceForStrategy命中情况蓝牙播放无声输出流patch没切过去看A2DP模块的profile是否完整录音采不到音input mixPort的profile不支持采样率核对配置文件的input route音量条变化但实际音量没变流未激活、index缓存未应用看setStreamVolumeIndex日志和AudioFlinger侧增益某些strategy声音异常小音量曲线数据不对或插值越界检查audio_policy_volumes.xml和VolumeCurves开机音频服务反复重启配置文件解析失败抓AudioPolicyService启动logcat逐行核对XML这些问题的共同点是表面现象在执行层根因在策略层。所以排查时一定从策略层日志看起不要一上来就怀疑HAL。6.2 调试命令和log开关AudioPolicyService有比较完整的日志开关排查时可以这样开adb shell setprop log.tag.AudioPolicy DEBUG adb shell setprop log.tag.AudioPolicyManager VERBOSE adb shell stop adb shell start抓日志时重点过滤几个tagadb logcat -v time | grep -E AudioPolicy|AudioFlinger|audio_hw系统状态用dumpsys看最直接adb shell dumpsys audio | grep -A 20 Audio policy adb shell dumpsys audio_policydumpsys audio_policy会输出当前的策略、设备状态、输出流信息、策略设备映射等关键信息。排查路由问题时我一般第一步就看它能快速判断系统认为当前连接了什么设备。改动配置文件后想要验证需要把改好的audio_policy_configuration.xml推到设备对应目录然后重启audio服务。这里注意不同设备原始配置的模块和profile差异很大直接从网上抄配置大概率翻车务必以原厂配置为基础增量修改。6.3 我踩过的一个典型坑拿一个真实的案例收尾。之前处理过一台设备USB声卡和蓝牙耳机同时接入时USB声卡总是抢不到输出用户插上USB音箱后声音还是从蓝牙走。当时先看getDeviceForStrategy的日志发现MEDIA策略的候选设备里USB排在蓝牙后面而且蓝牙的可用状态一直为true导致永远优先选蓝牙。查阅产品策略配置后发现该设备把USB设备放到了候选序列末尾调整顺序后USB音箱优先于蓝牙问题解决。这个案例给我的体会是Android设计策略引擎的初衷就是让路由选择可配置化遇到路由问题先改配置而不是改代码。配置驱动的好处是验证成本低改完XML推到设备重启服务就能看到效果比编译整个framework省太多时间。但前提是你要对设备本身的descriptor、profile烂熟于心否则改完可能引发所有声音都没了的连锁反应。做音频framework这条路没有捷径多读源码、多抓dumpsys、多记设备特征踩坑的次数自然就少了。
返回列表