ARTICLE DETAIL

资讯详情

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

Android音频底层核心服务:AudioFlinger架构与工作原理解析

Android音频底层核心服务:AudioFlinger架构与工作原理解析 Android AudioFlinger一——初识Android AudioFlinger很多做Android开发的同学做了几年应用层一碰到音频相关的疑难杂症就头疼。播放没声音、声音断断续续、外放切听筒没反应、延迟忽高忽低——这些问题表面上是上层App的逻辑问题但追根溯源最后都会落到同一个底层服务身上AudioFlinger。这个系列我打算从零开始围绕AudioFlinger把Android音频底层这套机制捋清楚。第一篇先聊它到底是什么在整个Android系统里处在什么位置为什么它值得你花时间研究以及刚接触时最容易踩的认知误区。这篇文章适合两类人看一是做应用层开发、经常被音频问题困扰想深入底层找根因的二是准备啃Android Framework源码、但不知道从哪里下手的。看完之后你至少能搞明白AudioFlinger的基本职责、启动流程、核心线程模型以及它和AudioPolicyService到底怎么分工。1. AudioFlinger是谁一次播放卡顿背后的总调度先从一个最常见的场景说起。你在手机上打开音乐App点了一首歌声音从扬声器出来整个过程看似简单其实经过了至少五六层模块协作。如果这时候另一App突然要播放提示音两个声音怎么混在一起如果声音要切到蓝牙耳机音频流怎么从原来的输出设备转过去这些都不是App自己说了算的幕后真正干活的就是AudioFlinger。AudioFlinger是Android音频子系统里一个核心的C服务官方定位叫音频策略的执行者。它负责所有音频数据的混合、路由和输出控制所有App产生的音频流最终都要汇聚到它这里由它统一调度写进声卡驱动。它的代码路径在AOSP里的位置是frameworks/av/services/audioflinger/和audiopolicyAudioPolicyService目录是邻居两者关系密切但职责完全不同。AudioFlinger能做什么我列几个核心点管理所有音频输出设备扬声器、耳机、蓝牙、USB等对应的混音线程每个打开的设备背后都对应一个PlaybackThread将多个App的音频流Track进行混音输出到同一个设备采样率、通道、位宽不一致时负责转换接收来自AudioPolicyService的命令比如把音频路由到蓝牙然后实际执行切换管理录音端的数据通路多个App同时录音时也要靠它来协调提供各种音频效果如均衡器、环绕声的宿主环境即Effect链的处理打个比方如果把Android音频系统比作一家电视台AudioTrack是各个栏目的编导AudioPolicyService是导播而AudioFlinger就是那个坐在音控室里、把各路信号接进来、调节音量、混成一路再推给发射塔的总控台。画面是不是一下就具体了。很多初学者以为音频是App直接写到声卡的这是最大的误解。Android系统基于安全考虑绝不会把声卡设备直接暴露给上层App。所有音频数据必须经过AudioFlinger这个核心节点由它做权限校验、格式转换、混音后统一写入/dev/snd/下的ALSA设备节点。理解了这一点很多上层现象就有了解释为什么AudioTrack的对象创建失败会报各种权限错误为什么不同App的声音能同时播放出来为什么某个音频流抢了另一个音频流的焦点——因为底层就是一台混音台。2. 藏着系统进程里的常驻服务启动流程与核心入口AudioFlinger不是App随便调用的东西它是一个常驻的系统服务。在Android 8.0之前AudioFlinger跑在mediaserver进程里和MediaPlayerService、CameraService等一起待着。8.0之后Google做了一个改动把音频相关的服务拆到了独立的audioserver进程里。这么改的原因也比较实在音频服务容易出问题如果还和Camera等服务混在一起一个崩了全崩拆开能让故障隔离同时也能更精细化地控制优先级和资源。这一节我们来梳理它的启动和初始化过程这是理解后面所有逻辑的基础。2.1 从init.rc到main函数系统开机时init进程会解析/system/etc/init/audioserver.rc拉起audioserver可执行文件。这个文件的入口在frameworks/av/media/audioserver/main_audioserver.cpp。里面做的事情不复杂创建AudioFlinger实例、创建AudioPolicyService实例然后把两个服务注册到ServiceManager。注册之后上层App就可以通过Binder拿到AudioFlinger的代理对象跨进程调用了。// main_audioserver.cpp 关键逻辑 int main(int argc __unused, char **argv __unused) { // 启动Binder线程池 spProcessState proc(ProcessState::self()); spIServiceManager sm defaultServiceManager(); // 创建AudioFlinger实例 AudioFlinger::instantiate(); // 创建AudioPolicyService实例 AudioPolicyService::instantiate(); // 处理Binder请求 ProcessState::self()-startThreadPool(); IPCThreadState::self()-joinThreadPool(); }这个文件要配合onFirstRef来看。AudioFlinger继承自RefBase当它的强引用计数从0变1时onFirstRef()会被调用很多关键初始化都在这里完成。比如创建音频线程、加载音频硬件模块、建立与AudioPolicyService双向通信的client——这些都不是懒加载而是服务一启动就立刻准备好。2.2 Binder接口IAudioFlinger的粗粒度设计AudioFlinger对外暴露的关键Binder接口是IAudioFlinger定义在frameworks/av/include/media/IAudioFlinger.h。客户端的AudioSystem类通过IServiceManager::getService(audio)拿到BpAudioFlinger代理再调用里面的接口。这个接口的设计粒度比较粗它不负责具体的数据传输只负责建立连接和下发指令。常见的IAudioFlinger接口有createTrack()客户端创建音频播放流时调用返回一个IAudioTrack对象后续数据的写入、播放控制都走这个子对象openRecord()对应的录音端入口返回IAudioRecordloadHwModule()加载某个音频硬件模块如primary、a2dp、usbsetParameters()下发厂商扩展参数比如切换通话降噪这类没有标准化接口的功能这个设计思路值得琢磨底层服务只提供粗粒度的入口具体的音频数据通路交给类似IAudioTrack这种更细粒度的对象。这样即使上层控制逻辑有调整也不会频繁改动AIDL接口定义。2.3 一个小坑两份系统服务别搞混早期的Android版本里AudioFlinger直接注册到ServiceManager之下服务名是media.audio_flingeriOSN之前或者硬编码在代码里的audio旧版。而现在新版本里AudioFlinger的ServiceManager注册名实际上是media.audio_flinger但更常用的是通过AudioSystem来隐式获取。源码里常会出现getService(audio)之类的东西这个不是AudioFlinger本身而是其他配套服务。不少新手在这里被绊倒过去ServiceManager里查audio这个服务名查出来一头雾水。我的建议是直接看源码在AudioSystem::get_audio_flinger()里会用getServiceIAudioFlinger(String16(media.audio_flinger))来拿Binder。以代码为准比到处查资料靠谱。3. 三个高频名词拆解Thread、Track与Mixer搞懂AudioFlinger必须搞清楚它内部几个核心对象之间的关系。我在各种技术群里看到很多初学者问到这几个概念时经常把它们搞混。这里一次性说清楚。3.1 PlaybackThread一个设备一条流水线PlaybackThread是AudioFlinger内部最核心的类之一代表一条播放流水线。每打开一个输出设备或者一种特定的输出模式AudioFlinger就会创建一个对应的PlaybackThread来管理这个设备上的所有音频流。举例来说扬声器外放对应一个MixerThreadPlaybackThread的子类蓝牙A2DP设备连接对应一个A2dpPlaybackThreadUSB音频设备对应一个MmapPlaybackThread如果打开了直接输出模式直通HDMI等会有DirectOutputThread每条PlaybackThread内部会有一个循环循环里做这几件事从自己管理的Track里取数据、混音、格式转换、写到声卡设备。这个循环依靠声卡的write函数来节流——声卡不消费完循环就堵住等待。理解这个之后你就能明白为什么蓝牙音频切歌时会感觉慢半拍或者震动器提示音不太同步了。不同设备各管各的循环切换过程需要重新建立新的线程中间必然有状态迁移和缓冲这是物理层面的限制。3.2 Track一条音频流的身份证和数据管道Track更准确地说PlaybackThread::Track代表一条从App到混音器的单向音频流。上层创建一个AudioTrack对象时底层经过Binder调用createTrack()最终在AudioFlinger里对应一个Track对象。一个PlaybackThread上可以挂很多Track它们通过trackId区分。Track主要有几件事保存这条音频流的格式信息采样率、通道数、格式PCM或压缩流维护一个环形缓冲AudioBufferProvider上层写的音频数据先落在这个缓冲区里等待PlaybackThread取走保存音量、播放速度、左右声道平衡等参数记录状态暂停、停止、主动播放等平时我们Debug用的dumpsys media.audio_flinger输出的核心内容就是列出所有线程以及每个线程上的所有Track状态包括它们是否是活跃状态、写入了多少帧、延迟多少这些信息在排查音频卡顿时非常有用。3.3 Mixer混音其实就是加权加法当多个Track同时播放时MixerThread在每次循环中会把所有活跃Track的音频数据按权重相加生成一路混音结果然后写进声卡。混音的原理并不复杂PCM数据本质上是一连串的采样值混音就是将它们按位相加。但要注意多个流相加可能超过16bit的表示范围所以参与混音的每个流要按系数缩放即各个Track有独立的trackVolume不同采样率的音源要单独做一次重采样SampleRate Conversion简称SRC统一到设备输出采样率不同通道数的数据要按通道映射规则扩展或折叠比如单声道扩展到双声道时两个通道的内容相同实际代码里AudioFlinger针对不同CPU指令集ARM NEON、x86 SSE等做了很多SIMD优化比如framework/base里的AudioMixer实现对int16和float两种格式都有专门的手写汇编加速。你不需要把汇编细节全部搞清楚但至少要知道混音不是无代价的——路由到同一个设备的音频流越多MixerThread的CPU消耗就越高。有些低端设备播放两个以上音频时发热严重一部分原因就在这里。3.4 录制的逆过程RecordThread播放是混合录制就是分离。RecordThread负责从一个输入设备读取PCM数据然后分发到各个Track此时叫RecordTrack。多个App同时录音时AudioFlinger会开启多个RecordTrack从同一个RecordThread里消费数据。要注意的是大多数输入设备只支持一个固定的输入格式所以多个录音流格式不一致时也需要做重采样和通道转换。Android 10以后增加了AudioRecord的并发策略同一时间多个App录音不再报错以前在某些设备上会因为权限问题互相挤掉但底下仍然依赖AudioFlinger的混音和分发逻辑。这里也是换声器、会议录音等场景比较容易出bug的地方。4. 决策与执行的分离AudioFlinger与AudioPolicyService的分工初学者常把AudioFlinger和AudioPolicyService混为一谈实际上这是两个独立服务只是都在audioserver进程里启动。理解它们的职责边界是吃透Android音频架构的核心。一句话概括两者的关系AudioPolicyService负责决策AudioFlinger负责执行。4.1 AudioPolicyService什么时候该出声从哪个设备出AudioPolicyService的核心工作是管理音频策略。它手里有一套音频策略规则由audio_policy_configuration.xml等配置文件描述知道当前系统有哪些音频设备、哪些音频流类型音乐、铃声、通话、导航、系统提示音等以及在不同场景下这些流应该路由到哪里。比如你插上耳机时音乐从扬声器切到耳机这个决策是AudioPolicyService做的。它还要考虑音频焦点如果正在打电话时来了一个通知音通知音是否该降低音量duck也是它的管辖范围。AudioPolicyService内部还有一个关键组件叫AudioPolicyManagerC侧所有策略判断的核心逻辑都在这。源码路径在frameworks/av/services/audiopolicy/managerdefault/下面。它根据设备的变化、流的激活状态、配置的策略矩阵算出此时应该把音乐流路由到A2DP蓝牙然后向AudioFlinger发指令把对应PlaybackThread的路由切过去或者重新打开一个输出。4.2 AudioFlinger接到指令后怎么执行AudioFlinger不会自己判断该不该切设备它只负责执行。当AudioPolicyService决定要切换路由时一般有两类情况设备还开着只是改变路由参数调用AudioFlinger的setOutputDevice()内部更新对应PlaybackThread的路由标志音频数据后续就写到新设备上设备没开需要新建调用openOutput()AudioFlinger创建一条新的PlaybackThread并通知底层音频HAL打开这个设备决策和执行的分离有很多好处。最重要的一点是厂商定制时可以单独替换策略层而不动执行层。比如某些手机加入了智能场景音频识别大部分改动集中在AudioPolicyService的规则配置里AudioFlinger的代码基本不用大幅修改。做系统定制的研发同事很多工作都花在了改audio policy的XML和AudioPolicyManager的细节上原因就在这里。4.3 仍然常见的坑dumpsys media.audio_flinger与dumpsys media.audio_policy排查音频问题时经常要抓这两份dump来对照。很多人用错命令导致排障效率很低。dumpsys media.audio_flinger看的是AudioFlinger里各路线程和Track的状态能反映底层数据通路的运行情况。比如某个App的Track是否active、是否有underrun计数在涨、当前设备采样率是多少都在这里。dumpsys media.audio_policy看的是AudioPolicyService里的策略状态能看到当前各种音频流路由到哪个设备、音量级是多少、客户端注册了哪些监听。建议遇到外放没声音切耳机无反应这类路由问题先把audio_policy的dump抓出来看路由决策再把audio_flinger的dump拿来核对底层线程在不在跑。很多时候光看一层会觉得一切正常对不上号才意识到是层与层之间的配合出了问题。5. 一条播放链路的完整旅程从AudioTrack到声卡概念讲完了我们从头到尾走一遍一条典型播放流的完整旅程。这个过程能帮你把前面散落的知识点串起来形成完整的地图。5.1 创建阶段App发起AudioFlinger注册第一步App调用AudioTrack.Builder.build()创建播放对象内部会调用AudioSystem::createTrack()。这个函数通过Binder到达AudioFlinger最终调用到AudioFlinger::createTrack()。AudioFlinger收到后要做的事根据调用方传进来的audio_stream_type音乐、导航、通话等和output信息找到或创建一个合适的PlaybackThread校验App权限、检查AudioFlinger的全局资源限制创建Track对象放进PlaybackThread的Track列表中返回一个IAudioTrack的Binder对象给App注意这里创建成功不代表设备马上开始出声它还只是一个注册动作。客户端拿到IAudioTrack后处于STOPPED状态数据通路还没打通。5.2 写入阶段共享内存与环形缓冲区其实AudioTrack和Track之间的数据传递不使用Binder逐帧传送那样效率太低。真实机制是在createTrack阶段AudioFlinger会通过MemoryDealer为每个Track分配一块共享内存App往里面写数据AudioFlinger从里面读数据。这块共享内存被封装成AudioTrackShared结构里面主要包括一个环形缓冲区环形队列App作为生产者往里面写PCM帧一个控制区audio_track_cblk_t存放读写指针、状态标志、播放位置等信息App端写入时直接操作这块共享内存只通过一次轻量级的futex唤醒来提示AudioFlinger有数据了。这个机制设计得相当高效实测下来的最大好处就是跨进程几乎没有拷贝开销也避免了Binder大量数据拿锁的性能瓶颈。5.3 播放阶段MixerThread的循环所有数据准备好后关键在于MixerThread里的主循环。我简化下这个循环的核心行为你能看到它其实就是一个高频运行的搬运工// 概念性伪代码展示MixerThread主循环的结构 void MixerThread::threadLoop() { // 等待被唤醒或者定时器到期 while (!exitPending()) { // 1. 收集当前所有活跃的Track // 2. 对每个Track做音量、重采样、格式转换 // 3. 把所有Track的音频数据混音到一段缓冲区 // 4. 通过音频HAL写进声卡 // 5. 根据硬件时钟计算下一次循环的时间 } }这个循环的速度由声卡的硬件周期决定。常见的fast mixer快速混音器周期在5-10毫秒左右普通线程更长一些。代码里会用write函数的实际阻塞时间来推算系统的音频时钟这对于维持稳定的播放节拍很重要。一个容易搞混的概念是低延迟和混音线程的关系。Android有专门的fast mixer机制对应FastMixer线程它通过MMAP内存映射直通模式绕过部分软件混音专门用于对延迟敏感的场景比如游戏音效、触摸提示音。普通MixerThread延迟高但功能全FastMixer延迟低但支持的条件苛刻。后面有精力的话我可以单独写一篇聊聊fast mixer的实现细节。5.4 结束阶段Stop、Release与资源回收App播放完毕调用release()时IAudioTrack会销毁对应的Binder对象AudioFlinger这边会从PlaybackThread的Track列表中移除这个Track释放共享内存。如果该Track是最后一个活跃TrackPlaybackThread有机会进入空闲状态并等待销毁或者被复用给下一次播放。这里有一个隐藏的坑有些App播放完没有正确调用release()或者持有了AudioTrack对象不放那么底层对应的Track就会一直挂在PlaybackThread上白占资源。长时间积累下来会导致播放线程维护的Track太多循环每次都要做空转检查CPU和功耗都有可观察的上升。这也是为什么很多性能优化课都会强调Stream资源必须及时释放。6. 实测验证与代码踩坑怎么确认你的理解是对的懂了概念之后强烈建议动手验证一下。这里分享几个我自己实践过的验证手段都能在本机直接跑不需要特殊硬件。6.1 用dumpsys观察真实AudioFlinger状态连接好adb后执行adb shell dumpsys media.audio_flinger你会看到一长串线程信息。找其中一个有活跃Track的线程观察每个Track下的字段Active标志表示这个Track是否正在被混音器消费Session ID和App创建的AudioTrack一一对应Format/Sampling rate/Channel mask这条流的实际数据格式Underrun count如果这个数值持续增长说明App写入数据的速度跟不上播放消费就会出现破音和卡顿实际操作一次用手机播放一首歌同时执行上面的dumpsys再切到蓝牙耳机如果支持继续播放对比前后控制台输出的线程列表和路由变化你会对切换设备时AudioPolicyService下发指令、AudioFlinger建新线程有非常直观的感受。6.2 抓日志验证启动顺序用下面一组命令清空日志后再播一次音观察关键节点的日志adb logcat -c adb logcat -v threadtime | grep -E AudioFlinger|AudioPolicyManager正常播放一次提示音你会看到类似AudioFlinger::createTrack、setOutput等日志。配合-b system、-b events缓冲区还能看到音频事件的上层链路。看日志时有一个细节值得注意Android 10以上默认日志级别可能过滤掉verbose级别的音频信息如果觉得信息不够可以查看ro.audio.logger等系统属性是否被打开。不同产商ROM对日志的输出控制差异很大这一点在排查问题时很容易踩坑。6.3 自己动手写一个最小Java层测试不用编译系统镜像直接在Android Studio里写个很小的Demo就能验证一个底层现象创建多个AudioTrack对象同时播放不同频率的声音它们能同时出声噪杂混乱但互不中断。这说明AudioFlinger的混音确实起作用了而不是后创建的流抢占前一个流。用极简的方式亲眼验证混音、验证路由比读多少文档都有用。// 极简同时播放两个音源的逻辑 // 使用相同的AudioTrack参数分别new两个对象写入不同频率的PCM数据 AudioTrack[] tracks new AudioTrack[2]; for (int i 0; i tracks.length; i) { int sampleRate 8000; int minBufSize AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT); tracks[i] new AudioTrack.Builder() .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioContentType.CONTENT_TYPE_MUSIC) .build()) .setAudioFormat(new AudioFormat.Builder() .setSampleRate(sampleRate) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .build()) .setBufferSizeInBytes(minBufSize) .build(); byte[] data generateSineWave(/* freq1 for 0, freq2 for 1 */); tracks[i].write(data, 0, data.length); tracks[i].play(); }我自己第一次跑这个Demo时最惊喜的一点是两个AudioTrack同时写入播放完全没有像某些简单硬件那样发生数据覆盖而是清清楚楚地混在一起输出。这一刻你对AudioFlinger是总调度台这句话的理解会一下子落到实处。7. 学习路线建议怎么继续深入AudioFlinger这是系列的第一篇能坚持到这里说明你对底层还是真有好奇心的。最后给几条直接可操作的学习路线建议按优先级排下来先掌握AudioTrack和共享内存机制。不必一上来就扎进AudioFlinger几千行核心代码里。先把上层的AudioTrack源码读透重点是obtainBuffer和写数据路径的环形缓冲实现由于它和底层的共享内存结构是直接交互的读懂它你就已经理解了一半的AudioFlinger数据流。本地有AOSP源码时按调用链追。下载对应Android版本的源码后跟踪一次AudioTrack::start()到AudioFlinger::createTrack()的完整调用链。用IDE的调转功能一层层点进去比看第二遍文档有用得多。重点突破MixerThread和FastMixer两个类。MixerThread::threadLoop()是音频播放的中枢先理解它的过程再深入AudioMixer的实现。FastMixer涉及更复杂的内存映射和实时调度的知识可以作为第二阶段目标。结合音频HAL看硬件交互。AudioFlinger和硬件之间还有一层HAL接口hardware/libhardware/include/hardware/audio.h。理解了HAL的open_input_stream、open_output_stream、start、stop等接口能更容易理解AudioFlinger里的打开设备到底打开了什么。大部分厂商定制的音频问题最后都会暴露在HAL层之上。多看一次运行时的真实dump。没有比实际设备更能检验理解是否正确的东西。每次分析一个音频问题都先抓一遍media.audio_flinger的dump坚持几次对Track、Thread、设备路由这几个概念的把握会有质的提升。AudioFlinger这块内容非常深一次根本讲不完。这篇算是地图和门槛后面我会按主题展开混音细节、低延迟播放、录音路径、音频效果框架、以及与AudioPolicyService的交互场景每一块单独成篇。如果你在搭建环境或者读源码时踩了什么坑也欢迎随时交流——很多我看代码时忽略的细节都是和被问题卡住的同学聊的时候重新挖出来的。
返回列表