Android 10音频播放链路剖析:MediaPlayer如何驱动AudioFlinger 写Android 10.0 Audio的分析已经到第三篇了。前面两篇把MediaPlayer的状态机、PlayerBase、音量焦点的脉络梳理过一遍到了这一篇我想把镜头从应用侧挪到系统侧MediaPlayer把播放请求交出去之后AudioFlinger内部到底发生了什么。这篇主要围绕一条主线展开——MediaPlayer如何创建AudioTrack、AudioTrack跨进程进入到AudioFlinger、AudioFlinger的线程模型如何处理播放数据再到最后输出到设备把每个关键节点的“为什么”讲清楚。这篇文章适合正在做系统音频定制、音视频播放器开发或者被播放爆音、延迟高、焦点抢占这类问题折磨的开发同学。我不会把源码一行行贴完而是把调用链和关键类整理成一张可以直接用的知识地图。如果你正在跟一个陌生音频项目按这个地图去翻代码会省掉很多瞎翻的时间。1. AudioTrack是怎么从MediaPlayer手里接棒的1.1 应用层最后一段播放代码长什么样很多人以为MediaPlayer内部直接new了一个AudioTrack实际不是这么简单。MediaPlayer通过MediaPlayerService往底层走时中间夹着一个叫AudioOutput的类它实现了AudioSink接口AudioTrack藏在它内部。关键调用链大概这样MediaPlayer::start() - MediaPlayerService::Client::start() - AudioOutput::start() - AudioTrack::start()AudioOutput才是真正管理音频输出的对象。它持有AudioTrack实例负责处理音量、焦点回调、采样率匹配这些事。设计上之所以要加一层AudioSink抽象是因为MediaPlayer作为一个多媒体播放引擎需要把“解码”和“渲染”解耦。今天底层用AudioTrack明天如果想换成别的输出通道只要换掉AudioOutput这一层实现就行不用动MediaPlayer状态机。我在实际调试中习惯先把AudioOutput这个类找出来看因为很多“播放没声音但状态正常”的问题最终都定位在AudioOutput的参数传递上。1.2 构造AudioTrack时的那些参数为什么不能乱填AudioOutput在内部openAudioSink时会去构造一个AudioTrack构造函数里有几个参数非常关键直接影响声音效果和延迟。第一个是frameCount。这个参数代表AudioTrack的环形缓冲区能容纳多少帧数据。帧数设置太小应用侧写入数据的速度一旦跟不上系统侧消费速度就会出现underrun声音断断续续或者爆音帧数设置太大延迟会变高播放器容易出现启动慢、暂停恢复反应迟钝的问题。我见过不少开发为了追求低延迟把frameCount压得很极限结果播放起播时反复咔哒响这就是典型的缓冲深度不够导致的。第二个是sampleRate。你传的采样率和AudioFlinger实际输出采样率不一致时系统会做一次重采样。重采样是有代价的如果不是特别需求建议传0让AudioFlinger帮你选最省事的路或者直接用解码器出来的原始采样率。第三个是audioAttributes。很多初学者喜欢用streamType但Android 10里系统已经全面转向使用audioAttributes。它决定音频属于哪个策略策略最后会直接影响走哪个输出设备和音量调节挡位。如果你在定制系统时发现自己的播放声音总是走错通道大概率是audioAttributes里的usage与产品场景不匹配。注意MediaPlayer场景下AudioOutput构造AudioTrack时会传入一个playerSessionId这个id贯穿整个AudioFlinger服务端用来标识这条播放链路。Session相关的日志排查在dumpsys里经常出现千万不要忽略。2. createTrack的旅程从IPC到共享内存2.1 路由先行音频数据还没生成走向已经确定了AudioTrack构造过程中有一个顺序很关键先做输出路由选择再创建Track。这两个步骤对应源码里的调用AudioSystem::getOutputForAttr() - AudioSystem::createTrack()getOutputForAttr这一步本质上是向AudioPolicyService询问我这个音频流应该送到哪个输出设备、走哪个输出管线。AudioPolicyManager会根据audio_attributes里的usage、streamType、设备状态去做策略匹配。Android 10里音频输出以outputId作为身份标识。每一个outputId背后对应一个PlaybackThread和一个音频HAL的stream。对MediaPlayer来说最常打交道的三个选择是primary输出默认主播放输出用在铃声、按键音的同时参与混音。deep_buffer输出专门为媒体播放设计的低功耗路径数据量大延迟高一些。raw输出要求线性PCM直接送给HAL不参与深度混音延迟较低。如果你发现播放音乐时的延迟莫名其妙偏高先查一下是不是被路由到了deep_buffer。这是很多“音乐播放有延迟感”问题的真正元凶不是代码处理慢而是走的管线本来就不是低延迟设计。2.2 AudioFlinger端createTrack_l从请求到内存分配getOutputForAttr拿到outputId后应用侧通过binder往AudioFlinger发起createTrack调用。AudioFlinger端会先找到outputId对应的PlaybackThread然后执行以_l结尾的线程内函数比如createTrack_l。createTrack_l里做的事情串起来大概是校验参数比如采样率、通道掩码、音频格式是否合法。检查是否允许走fast track以及是否能复用已有的mixer通道。创建一个Track核心对象为它在线程的track列表里占位。分配一块共享内存作为AudioTrack和AudioFlinger之间的数据交互区。返回一个TrackHandle代理对象应用侧拿着它执行后续的start、pause、setVolume等操作。这里有个很重要的概念需要搞清楚App端拿到TrackHandle后并没有拿到一块可以直接写数据的HAL buffer它的所有操作本质上都是往AudioFlinger发IPC请求。真正播放数据的“客户区”是共享内存。2.3 共享内存控制块一个播放周期的公告栏为什么Android播放音频时不用每帧数据都走一次binder传输原因很简单binder传输有开销音频数据量又大如果每帧都走IPCCPU开销根本扛不住。因此AudioFlinger在createTrack阶段会准备一块内存通过android的ashmem机制映射给应用侧。这块内存里包含一个audio_track_cblk_t控制块和一段环形缓冲区。控制块里有写位置、读位置、状态的原子变量App往环形区内写PCM数据并更新写指针AudioFlinger线程从环形区里读数据并更新读指针。可以把这块共享内存理解成一个公告栏App把音频数据放在公告栏里写完之后在控制块上通知AudioFlinger“我写好了”AudioFlinger混音线程看到通知就来消费数据。整个过程没有大量的数据复制只有指针和原子变量的同步。注意共享内存的读写采用生产者消费者模型环形buffer写满时应用侧要等消费跟不上时缓冲就会空转。dumpsys media.audio_flinger输出里的underrun计数器就是用来反映“消费者没东西可读”次数的调播放起播问题时建议先把这项看透。3. AudioFlinger线程模型音频数据在系统侧走哪条路3.1 MixerThread绝大多数播放任务的主战场默认情况下所有普通的PCM播放都会被分配到MixerThread。MixerThread的核心是threadLoop它是一个不断循环执行的线程函数每一轮大概做这些事计算本轮的SleepTimeUs prepareTracks_l()逐个检查活跃track的状态处理音量ramp、buffer消耗进度 threadLoop_mix()把多个track的数据混音到一起 mOutput-write()把混合后的数据写到HAL层最终送给设备prepareTracks_l是最容易被忽略但最容易出问题的一步。它负责遍历当前线程上所有track判断哪些track需要继续参与混音哪些track已经数据耗尽等待补充哪些track需要做音量渐变。这里每一帧的“帧”不是视频帧而是音频采样帧一帧包含多声道各一个sample。MixerThread里做得最多的是音频混音。不同采样率的track要统一重采样到输出采样率不同音量大小的track要按系数混合。如果你播放的文件采样率是44.1kHz而设备输出端要的是48kHz那这里就会启动重采样器完成转换。这里有一个经验如果应用侧指定了一个不常见的采样率混音线程的CPU占用会明显上升原因就在重采样开销上。还有一点值得注意MixerThread每隔一段时间会计算本轮的sleep时间。没有活跃track时它会正常休眠避免线程空转造成功耗浪费。有活跃track时则按一个精简的周期唤醒。这个机制让系统在“别吵”和“别卡”之间找到平衡。3.2 FastMixer给低延迟场景开的快车道MediaPlayer不是低延迟链路的主力场景但理解FastMixer对排查问题很有帮助。FastMixer是MixerThread内部衍生出来的一条特殊路径专门供那些带FAST标志的AudioTrack使用。它有自己的独立线程以极短的周期去采样fast track的数据并直接写入HAL设备从而减少缓冲区拿数据的时间降低延迟。FastMixer的引入解决了普通MixerThread在慢周期里难以兼顾低延迟track的问题。普通track走慢速主循环fast track走轻量级快速循环最终汇合到同一个HAL输出流上。不过FastMixer的准入条件比较苛刻track必须声明允许fast、输出线程本身支持fast mixer、帧周期要与HAL端匹配条件不满足就自动降级。所以你在调MediaPlayer的时候如果发现某个track没有走FastMixer不一定是代码配错了很可能是硬件或线程策略不允许走这条路。Android 10里MediaPlayer如果走的是deep_buffer输出那它就基本和FastMixer无缘了。3.3 Offload和MMAP线程另外两条不太常走的路MixerThread之外还有两类播放线程容易被忽略一个是OffloadThread一个是MMAP线程。OffloadThread用于硬件解码播放场景。应用侧传入压缩格式的音频数据比如mp3、AAC系统不做PCM混音而是直接把压缩数据交给DSP或codec硬件去解码输出。这条路的好处是CPU占用极低弊端是系统管控力变弱很难对多路音频做精细混音。很多追求功耗的设备厂商会把音乐播放默认引导到Offload路径上。MMAP线程则是通过共享内存映射方式实现超低延迟播放。应用侧直接映射一块可读写的内存给音频服务与HAL延迟可以压到非常低。它主要服务于专业音频App和低频延迟要求极高的场景。如果MediaPlayer突然出现在MMAP线程上一般不是正常现象多数是代码显式指定了低延迟播放模式。下面这张表汇总了几种线程类型的差异方便你排查问题线程类型主要用途适用场景延迟特点混音能力MixerThread普通PCM多路混音播放音乐、提示音、游戏中等强FastMixer低延迟快速播放路径按键音、低延迟音效低有限OffloadThread硬件解码播放在线音乐、本地压缩音乐中等弱MMAPThread共享内存直通播放专业音频、超低延迟需求极低弱3.4 混音输出路上的音量渐变每次调整音量时如果直接把音量系数从0切换到0.5会表现为一个突然的跳变在扬声器上就变成“啪”的爆音。AudioMixer处理这个问题的方式是音量ramp在多个混音周期内把目标音量从一个值平滑过渡到另一个值。这也解释了为什么你在AudioFlinger里调整音量时偶尔会观察到音量值并不是瞬时到位而是分几步逼近目标值。ramp逻辑本身不复杂但如果你在车机或语音项目里频繁切换音量这部分的细节非常影响听感。很多主观反馈的“声音刺耳”“有咔哒声”最后查下来都是音量ramp配置不当。4. 音量、焦点与路由数据流之外的三道闸门4.1 路由决策的幕后功臣AudioPolicyManager播放链路里最容易被忽略、但影响最大的其实不是数据怎么传而是路由怎么选。在getOutputForAttr的调用背后是AudioPolicyManager在做出一连串的决策。它会先根据AudioAttributes里的usage和contentType去匹配一个product strategy然后在该策略允许的输出设备列表里根据当前连接状态选出一个合适的设备。这个设备再对应一个outputId返回给调用者。整个过程在framework层只表现为一次binder调用但内部要遍历大量策略和设备配置。如果你想快速判断一个播放流最终去了哪个设备可以用dumpsys audio去查输出里会有当前各音频流的设备关联信息。调试路由问题时间长了你会养成一个习惯先看策略再看设备最后才怀疑代码。4.2 Android 10的焦点机制怎么影响你的MediaPlayer音频焦点是应用层协同的一种机制但它的结果会传导到AudioFlinger。当你的MediaPlayer正在播放时如果另一个应用请求并获得焦点你的应用会收到焦点丢失回调。焦点丢失分两类。一种是暂时失去焦点比如导航语音播报系统允许继续播放但可能需要降低音量或者短暂暂停另一种是永久失去焦点比如另一个播放器开始播放此时通常应该暂停并释放焦点。我遇到不少问题案例应用收到暂时焦点丢失回调后没有处理或者处理成永久暂停导致导航语音播报完毕后音乐也没有恢复。反过来有些应用被duck降音后没有恢复原音量听起来就像播放器音量莫名变小。处理焦点状态的原则很简单根据回调类型决定下一步动作而不是一刀切。4.3 音量从应用设置到扬声器听懂的过程音量在Android里分多层应用层调节的是AudioTrack内部音量系统层调节的是stream volume最后还有设备音量三者共同作用在最终输出上。AudioFlinger在混音时会根据track对应的stream类型去查询当前系统音量换算成dB值然后作用在这个track的混音系数上。如果系统音量很低无论应用音量调多高最终都很小。这也是有些“应用音量拉满但声音还是小”问题的真正原因系统音量或设备音量不在预期位置。提示排查音量问题时先做一次系统音量排除然后测试不同track之间的音量差异。如果单track音量异常多发生在AudioFlinger的混音系数计算上如果全局音量异常先看系统音量管理模块。5. 调MediaPlayer音频链路时踩过的几个坑5.1 起播爆音和低延迟之间的博弈播放刚开始时出现“啪”一声爆音是我在音频调试中最常遇到的现象。原因通常是缓冲还没有填够数据系统侧就开始消费并输出首帧数据里夹杂了大量无效零值或者不连续的数据人耳听到的就是一个刺耳的爆点。解决办法是给AudioTrack留出足够的pre-roll时间让写入端先往共享内存里写满若干帧再触发start操作。AudioTrack内部本身有启动门限的逻辑但应用侧如果在数据没有准备好的情况下过早调用start还是会暴露问题。MediaPlayer场景下别把frameCount压得太极限否则每次起播都是在赌线程调度。5.2 焦点中断后声音“回不来”的问题某个播放器项目里出现过这样的事正在放歌来了条微信语音语音说完之后音乐声音明显变小而且一直没有恢复。查了代码才发现问题不在AudioFlinger而在应用层焦点处理逻辑焦点丢失回调把音量调低了但焦点恢复后没有重新设置音量。另一个常见问题是焦点恢复后直接调用start但没有重新请求焦点。如果此时系统侧焦点已经被其他应用占着播放器虽然会出声但会被系统持续降权表现就是播放一下就断或者声音很小。处理这个问题的正确姿势是恢复播放前重新走一次音频焦点请求流程拿到焦点后再执行start。5.3 快速排查表把常用的排查思路整理成一个速查表遇到问题可以按着顺序看症状第一步排查动作常见根因完全没声音dumpsys media.audio_flinger看playback线程状态路由错误、焦点被抢占起播爆音看buffer的underrun计数和写指针趋势frameCount过小、pre-roll不足声音断续检查underrun次数与CPU负载写入不及时、线程被抢占延迟感明显查路由用的输出类型走到了deep_buffer路径音量忽大忽小查看dumpsys audio里stream音量焦点duck没有恢复排查音频问题最大的忌讳是直接改代码试错。音频链路环节很多从应用到AudioFlinger再到HAL每一层都可能出错没有日志就动手改往往会把问题改复杂。先抓一份系统状态把路由、焦点、音量这些基础项都确认过之后再针对性去改代码效率会高得多。最后分享一个我一直在用的习惯拿到音频问题先别急着看App代码先拉dumpsys media.audio_flinger和dumpsys audio看输出线程的状态、track数量、underrun计数。大多数MediaPlayer播放类问题真正的原因其实都在AudioFlinger这条链路上只是最后表现在App层的回调里罢了。音频调试说到底是在跟时序和状态打交道把状态摸透了问题就解决了一大半。