ARTICLE DETAIL

资讯详情

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

MediaPlayer与AudioTrack:Android音频播放底层差异与选型指南

MediaPlayer与AudioTrack:Android音频播放底层差异与选型指南 做Android音频开发尤其是做播放器、音效类App几乎都绕不开MediaPlayer和AudioTrack这两个类。很多初级开发只知道“MediaPlayer能播MP3AudioTrack也能播一个简单一个复杂”但真到了选型时并不知道两者底层差在哪一旦项目做深了各种诡异问题就跟着冒出来。MediaPlayer是系统提供的一套“开箱即用”播放器AudioTrack则是更贴近底层的PCM输出通道。你可以粗略理解成前者是去餐厅点菜后者是自家厨房备菜。这篇文章我从API设计、播放流程、延迟表现、格式支持、实际踩坑这几个维度把这两种音频播放方式彻底捋一遍。不管是刚入门做小Demo的新手还是准备给播放器重构的老手都能从这里找到值得参考的东西。1. 两种方案的定位差异为什么Android需要两套音频播放API1.1 MediaPlayer系统替你打点好一切的“全能播放机”MediaPlayer在Android多媒体框架中属于高层API它内部集成了音频解码器选择、音效处理、音量调整、音频焦点、音频路由等一大堆系统级逻辑。你用MediaPlayer播一个MP3或者AAC只需要给个路径或URL剩下的事基本不用操心系统会从硬件解码器、软件解码器里选一个合适的方案把编码音频解成PCM后写入底层输出设备。MediaPlayer本身就是一个“状态机”从Idle到Initialized、Prepared、Started、Paused、Stopped、PlaybackCompleted再到Error每个状态之间都有严格的迁移条件。你要是不按顺序来抛IllegalStateException是家常便饭。整个播放链路从setDataSource开始到start播放内部实际是走NuPlayer或Stagefright这套解码管线。这套管线不光支持音频还能处理视频所以MediaPlayer也能播带画面的视频文件。它的好处是兼容性极强常见的MP3、AAC、FLAC、WAV、OGG都能播很多项目即使不需要视频也习惯用它图省事。但它有个绕不开的问题链路太长内部是黑盒。比如你做音乐播放器想精确控制播放位置或者对延迟有几十毫秒级的要求MediaPlayer基本没法满足。从解码到输出中间经过太多层你根本不知道系统在哪里缓存了多少数据。延迟一般都在一两百毫秒以上极端场景甚至能到几百毫秒。这种场景下MediaPlayer反而成了限制你发挥的那堵墙。1.2 AudioTrack绕开解码链路的裸PCM通道AudioTrack的定位和MediaPlayer完全不一样。它不负责解码不管你是MP3还是AAC它统统不认只认裸PCM数据流。它的职责就是把你喂给它的PCM字节按设定的采样率、声道数、位深持续写到音频设备上。从应用层来看调用链比MediaPlayer短很多你write数据AudioTrack内部通过AudioFlinger和Audio HAL把数据送到硬件中间几乎不做多余加工。正因为这样AudioTrack给了你极大的控制权缓冲区大小自己定、采样率自己指定可以用static模式一次性装载数据也可以用stream模式无限续写。你可以精确控制“什么时候往里面写多少数据”这在做乐器类App、实时语音、音效播放时非常关键。比如一个钢琴软件你按下一个键希望声音在几毫秒内出来MediaPlayer那种经过完整解码链路的播放器做不到AudioTrack或者更底层的AAudio才有戏。所以用一句话概括MediaPlayer是“帮你做决定”AudioTrack是“把决定权交给你”。如果你自己解决了解码环节比如通过MediaCodec、FFmpeg拿到了PCM那AudioTrack就是那个把PCM送出去的出口。1.3 题外话音频输出设备与驱动层的坑社区里经常能刷到“Realtek high definition audio一直没有输出设备”“NVIDIA high definition audio右键有个叉叉”这类问题这些和本文讲的应用层API其实是两个层面的东西。MediaPlayer和AudioTrack在应用层把PCM交给系统音频服务后后面还要经过音频路由、声卡驱动的层层传导最后才从耳机或扬声器出来。设备驱动一旦不对应用层做得再对也没声音。所以排查时先分清是App层、系统服务层还是驱动层的问题免得白忙活。后文所有的代码和排错默认都假设底层驱动和音频设备是正常的。2. 核心API细节与参数拆解2.1 MediaPlayer状态机用好这套约束少被异常折磨先说MediaPlayer的常规播放代码。一个最简单的本地音频播放流程是这样MediaPlayer player new MediaPlayer(); try { player.setDataSource(/sdcard/test.mp3); player.prepare(); // 准备 player.start(); // 开始 } catch (IOException e) { e.printStackTrace(); }这段代码能跑但它有两个问题一是prepare()是阻塞的如果文件比较大、或者传的是网络地址会卡住调用线程在UI线程调用直接ANR。二是很多人不知道MediaPlayer有状态约束比如你在调用setDataSource之前调prepare或者release之后再调start必然抛IllegalStateException。正式项目里我习惯用异步准备public class PlayerWrapper { private MediaPlayer mediaPlayer; public void play(String path) { if (mediaPlayer ! null) { mediaPlayer.release(); mediaPlayer null; } mediaPlayer new MediaPlayer(); try { mediaPlayer.setDataSource(path); mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(mp - { mp.start(); }); mediaPlayer.setOnCompletionListener(mp - { // 播完释放避免占用资源 mp.release(); }); } catch (IOException e) { e.printStackTrace(); } } }这里的重点不是代码多简单而是你要理解MediaPlayer内部各个状态的含义setDataSource之后进入InitializedprepareAsync之后进入Preparing准备完成回调进Preparedstart之后才是Started。你在Prepared之前调start就是非法操作。多状态机设计确实麻烦但反过来想这种约束其实保证了播放链路在并发场景下不会出现不可预期的错误。这也是Java里比较老练的设计思路理解它比死记API更有价值。2.2 AudioTrack核心参数bufferSize、采样率、声道、位深AudioTrack的构造函数有新旧版本新版本使用Builder模式但核心参数没变。以流模式为例int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_OUT_STEREO; int audioFormat AudioFormat.ENCODING_PCM_16BIT; int bufferSize AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioTrack audioTrack new AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, bufferSize, AudioTrack.MODE_STREAM ); audioTrack.play(); short[] data new short[1024]; while (playing) { // 把PCM数据写入data数组 audioTrack.write(data, 0, data.length); }这里面有个非常关键的参数是bufferSize。很多人以为随便填个1024或者4096就行实际上系统给了你一个底线getMinBufferSize()。它是根据采样率、声道、位深算出来的最小缓冲区低于这个值底层驱动来不及消费数据播放就会出现爆音、断续、UVunderrun问题。而如果缓冲区设置得太大延迟又会明显上升。所以性能敏感的场景里我往往在getMinBufferSize的基础上乘以1.5到2作为安全余量既保证不断流又不至于延迟高到不可接受。还有一个容易忽略的点AudioTrack的write()是同步阻塞的不是丢进去就立刻播。如果你在UI线程里循环write几秒钟的数据界面一样会卡顿。正常做法是单独开一个播放线程或者用线程池固定一个工作线程专门写数据。每次write返回的实际写入字节数也不一定等于你传入的长度你要循环判断直到把数据写完后面实战章节我会再展开讲。2.3 一张表看懂两者的核心差异对比维度MediaPlayerAudioTrackAPI定位高层播放器封装低层PCM输出接口输入数据编码音频MP3/AAC等或视频仅限裸PCM数据是否负责解码是内部自动选解码器否需要自己解码状态控制复杂状态机非法操作抛异常相对简单write/play/pause延迟表现较高通常100ms以上较低Java层可低至几十ms资源占用较高涉及解码器、音效等组件较低更可控灵活度低内部黑盒高可精确控制缓冲与数据音频焦点/路由可配合AudioManager处理需自己配合AudioManager处理典型场景音乐播放器、视频播放器、网络音频游戏音效、乐器App、语音处理是否支持播放网络流支持需要自己拉取/解码这张表基本把选型的大方向说清楚了。但实际项目里两者经常是“上下游”关系MediaPlayer在内部其实也会创建AudioTrack来输出最终PCM你自己用MediaCodec或FFmpeg解码拿到PCM后再用AudioTrack播放本质是把系统替你包办的那些环节拆开自己做。理解这层关系对排查很多深层的播放问题特别有帮助。3. 实战走一遍在线播放器与低延迟音效3.1 用MediaPlayer做在线音乐播放的完整流程做在线播放器最典型的坑是网络流。setDataSource传入HTTP URL然后prepareAsync这时候如果网络很慢用户点击播放之后会有好几秒“没反应”新手经常不知道去哪看进度。线上项目一般会在UI上做“加载中”状态并监听OnPreparedListener和OnErrorListenermediaPlayer.setOnPreparedListener(mp - { progressBar.setVisibility(View.GONE); mp.start(); }); mediaPlayer.setOnErrorListener((mp, what, extra) - { progressBar.setVisibility(View.GONE); Toast.makeText(context, 播放失败错误码 what / extra, Toast.LENGTH_SHORT).show(); return true; });如果你要支持后台播放还要把服务跑在ForegroundService里同时申请音频焦点。焦点处理的逻辑是AudioManager.requestAudioFocus如果焦点被其他App抢走比如来电、导航你要么暂停要么降低音量等焦点恢复再继续。MediaPlayer本身并不强制你做焦点处理但你要是不做就会出现“音乐和导航声音一起出来”的糟糕体验这在应用商店审核时也是扣分项。还有一个容易被忽略的点网络流播放到一半突然断网MediaPlayer会回调OnErrorListener错误码通常是MEDIA_ERROR_IO或MEDIA_ERROR_UNKNOWN。这时候不能直接手动重启播放正确的做法是先release清空状态重新new一个MediaPlayer再setDataSource。因为状态机可能已经进入Error状态直接start只会让你收获一堆异常。我之前就因为偷懒想复用同一个MediaPlayer实例做网络重试结果反复踩IllegalStateException后来老老实实走“销毁重建”流程问题才消失。3.2 用AudioTrack生成并播放正弦波AudioTrack最纯粹的用法是播放你自己生成或解码出来的PCM数据。下面我用一个生成正弦波的例子来说明它不像播放文件那样需要准备一堆资源能很直观地展示AudioTrack的工作方式。先生成数据再把数据写入AudioTrackint sampleRate 44100; double frequency 440.0; // A4音 int bufferSize AudioTrack.getMinBufferSize( sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT ); AudioTrack track new AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM ); track.play(); Thread t new Thread(() - { short[] buffer new short[bufferSize / 2]; double phase 0.0; double delta 2 * Math.PI * frequency / sampleRate; while (isPlaying) { for (int i 0; i buffer.length; i) { buffer[i] (short) (Math.sin(phase) * Short.MAX_VALUE * 0.3); phase delta; if (phase 2 * Math.PI) phase - 2 * Math.PI; } track.write(buffer, 0, buffer.length); } track.stop(); track.release(); }); t.start();这段代码虽然简单但里面藏着几个关键细节。音量我乘以0.3而不是1.0是为了给系统留一点余量免得设备输出端削波。缓冲区大小取bufferSize/2对44100Hz、单声道、16bit来说意味着每轮写入约23ms的数据。这个粒度是合理的不会因为单次写入太大而出现明显延迟也不会因为太小导致频繁唤醒线程。实际项目中你不会只播正弦波可能从FFmpeg解码拿到的是float型PCM或者多声道PCM这时候只需要对应修改audioFormat和channelConfig。只要喂给AudioTrack的PCM数据格式和构造参数一致播放就是顺的一旦不一致比如16bit数据写进了声称24bit的轨道结果就是破音或者完全噪声。这类问题排起来很费劲先核对数据格式往往是最快的路径。3.3 降低延迟的几个硬指标做乐器App、节奏游戏的人最关心的就是延迟。Java层AudioTrack的延迟表现常规优化后能做到50ms上下想再低就要上AAudio或者OpenSL ES这类更底层的接口了。如果你只能留在Java层建议抓住这几个点采样率尽量用设备的native采样率不要触发重采样。可以通过AudioManager.getProperty(PROPERTY_OUTPUT_SAMPLE_RATE)拿到当前设备的原生采样率。bufferSize不要设置得太大在getMinBufferSize基础上加一点余量就行。每多10ms缓冲延迟就多10ms这个账自己算。播放线程优先级调高让write尽量不被系统调度耽误。避免在write前后做耗时计算需要生成波形时提前用独立线程算好。能用MODE_STATIC的场合尽量用static模式一次性写入的短音效比stream模式效率更高延迟也更稳定。这几点看着简单实际操作时对体验的影响非常明显。同样的音效你把采样率从44100改成设备不支持的22050底层就得做SRC采样率转换声音可能变调、延迟变大用户一听就能感觉“不对味”。4. 场景选型决策两条路都摆着别走错4.1 选MediaPlayer更稳妥的典型场景做音乐播放器、音频播放列表、网络广播这类场景首选MediaPlayer。这些场景需要解码的格式多需要长时间稳定播放还涉及后台切换、焦点处理MediaPlayer开箱即用。你不需要去关心音频究竟是怎么被解码出来的系统会为MP3选对应的软件解码器为FLAC选无损解码器为不同设备配置选择最合适的解码方案。这对绝大多数App来说已经足够了。举个例子你想给App加一个“播放本地MP3”的功能用MediaPlayer大概几十行代码就搞定了。用AudioTrack的话你得先写一个MP3解码器或者引入FFmpeg库再把解码出的PCM往AudioTrack里喂光接入解码库和维护ABI适配就够你喝一壶了。这种场景还选AudioTrack就是典型的用大炮打蚊子开发成本高收益却不明显。4.2 选AudioTrack更合适的典型场景反过来如果你的App对音频时序有强要求或者你已经引入了自己的解码/处理链路AudioTrack就能发挥价值了。典型场景包括游戏打击音效要求按下攻击键瞬间出声音乐器类App比如电子琴、架子鼓需要快速响应实时变声、K歌混音需要把麦克风采集的数据实时播放监听音频可视化要边播放边拿频谱数据MediaPlayer给不了你原始PCM已经用FFmpeg/MediaCodec解码出PCM需要一个出口把数据播放出来我做过一个电子琴项目刚开始图省事用的MediaPlayer每个琴键都new一个MediaPlayer实例去播一个很短的音效文件。结果按下琴键到出声延迟肉眼可见快速连续弹奏时还会出现上一次播放还没释放、下一次就已经start的异常。后来改成AudioTrack加预先加载好的PCM采样数据延迟和稳定性立刻好了很多。有些东西只有做完对比才知道哪个方案是为哪种场景设计的。4.3 焦点、路由与音量两者都躲不开的公共议题不管选哪个API音频焦点和音频路由都是必须处理的。Android系统的音频是共享资源多个App可能同时在播。你如果不申请焦点来电时你的音乐还会继续放你不处理耳机插拔拔出耳机后声音可能直接从扬声器轰出来体验非常糟糕。处理方式上AudioManager是公共的控制中心AudioManager audioManager (AudioManager) getSystemService(Context.AUDIO_SERVICE); AudioAttributes attributes new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); AudioFocusRequest focusRequest new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(attributes) .setOnAudioFocusChangeListener(this::onFocusChange) .build(); audioManager.requestAudioFocus(focusRequest);MediaPlayer和AudioTrack在“焦点”这个层面没有本质区别焦点是系统全局的你的播放器需要自己根据焦点变化暂停、继续、降低音量。这也是为什么很多大型音乐App会专门封装一套“播放控制引擎”既管理MediaPlayer/AudioTrack又管理焦点和路由事件。把这两层分开理解之后你对整个Android音频系统的认知才算真正完整。5. 常见问题与排查技巧实录5.1 高频问题速查表现象原因排查方法MediaPlayer报IllegalStateException状态机未走到合法状态就调用start/prepare等检查调用顺序异步准备必须等回调音乐播放ANR在UI线程调用prepare()改用prepareAsync()播放爆音、沙沙声音频数据削波或AudioTrack缓冲太小降低幅值增大bufferSize声音变调采样率与设备输出采样率不一致改用native采样率避免SRC播放MP3只有噪声把编码数据直接喂给了AudioTrackAudioTrack只接受PCM先解码拔掉耳机后外放巨大声未注册路由变化监听注册AudioDeviceCallback调整音量连续快速播放偶发异常上一次播放未release就再次start每次先release再new实例后台播放被系统杀掉没有使用前台服务使用ForegroundService通知这张表是平时排查问题的核心索引下面我把几个最容易踩的坑展开细说。5.2 MediaPlayer的错误码与重试策略MediaPlayer的错误回调不是随便看看就完事的。MEDIA_ERROR_IO、MEDIA_ERROR_MALFORMED、MEDIA_ERROR_UNSUPPORTED每个类型背后的处理逻辑都不同。比如MEDIA_ERROR_UNSUPPORTED是解码器不支持当前格式你再重试一百次也没用正确做法是提示用户格式不支持。而MEDIA_ERROR_IO多半是网络问题可以在重试前先检查网络状态网络恢复后再重建播放器。经验总结下来就是错误码返回时MediaPlayer内部已经进入Error状态必须release后重新创建别想着调reset()后马上重试。reset对很多机型来说本身就是一个不稳定的操作不如直接销毁重建干净利落。我见过不少同学在这上面纠结代码写得又长又绕其实核心就一句话——重建对象。5.3 AudioTrack的静音、噪声与时序问题排查AudioTrack出问题很多时候是数据没喂对而不是播放接口本身的问题。比如你从某处拿到的PCM是32bit float而你创建AudioTrack时用的是ENCODING_PCM_16BIT那么结果要么爆音要么失声。遇到这种情况先确认三件事采样率对不对、声道数对不对、位深对不对。三个参数只要有一个和实际数据不一致输出就是垃圾数据。另外很多人在循环write时没考虑“返回写入字节数比传入少”的情况。如果剩下一部分没写完就继续生成下一批数据整体节奏会乱听起来就是一顿一顿的。正确写法是写循环当前批没写完就接着写直到全部写进去再生成下一帧。关于时序问题我额外分享一个经验如果播放线程和生成线程不在一个线程一定要加一个足够深度的队列。生产者生成数据的速率很可能比消费者稍快或稍慢没有队列缓冲就只能靠睡眠乱凑最终要么卡顿要么堆积内存。我实践中用一个容量为2到3个buffer的环形队列播放线程从队列取数据生产线程把数据放进去实测流畅很多。这种做法在实时音频处理里几乎是标配。5.4 别再忽略release资源泄漏的隐形炸弹最后要提一下release。很多初学者写完demo就丢一边了Activity销毁了也不管播放器还在跑结果内存、解码器、音频设备资源一直被占用。MediaPlayer要releaseAudioTrack也要release。而且release之后最好把引用置空防止回调里再次调用。特别是AudioTrack它持有的是系统音频服务那边的句柄不release不只是你自己App内存泄漏还会让系统音频服务越积越多最终影响整机音频稳定性。在测试设备上我见过连续创建AudioTrack不释放十几分钟后整个设备的音频都罢工了重启才恢复。这种事一旦发生在线上用户身上问题等级就是P0。正确姿势是在onStop、onDestroy以及其他生命周期节点做统一释放判断。项目里可以写一个AudioPlayerManager来统一管理public void release() { if (mediaPlayer ! null) { mediaPlayer.release(); mediaPlayer null; } if (audioTrack ! null) { audioTrack.stop(); audioTrack.release(); audioTrack null; } audioManager.abandonAudioFocusRequest(focusRequest); }我自己项目里有个很深的感触MediaPlayer和AudioTrack不是两个对立面而是同一个音频系统的两个层次。MediaPlayer帮你把解码、状态管理都包了适合快速实现常规播放AudioTrack把最终输出通道交给你适合做定制化、低延迟的音频处理。早期做乐器App我一开始迷信MediaPlayer省事被延迟折磨了一轮换AudioTrack轻松解决后来做视频播放器又发现不该强行用AudioTrack去解码推流还是回到MediaPlayer来得快。如果你现在正纠结到底用哪个先把你的音频数据格式列出来再想清楚是延迟优先还是开发效率优先答案基本就出来了。音频这条路坑很多希望上面这些踩坑记录能让你少走几步弯路。
返回列表