音视频播放是Android开发中非常常见的需求,无论是短视频、音乐类应用,还是电商直播、在线教育,都离不开播放器。系统提供的MediaPlayer是绝大多数开发者接触的第一个播放组件,它封装了音频解码、视频渲染、流媒体协议等底层能力,用很少的代码就能实现一个可用的播放功能。但它同时也是一个出名的坑王,状态不对、时序不对、资源没释放,都会带来莫名其妙的崩溃。这篇文章会把MediaPlayer的完整使用流程、视频渲染方案以及避坑要点讲清楚。

一、MediaPlayer的基本用法与状态机原理
MediaPlayer采用的是典型的状态机设计,理解状态机是用好它的前提。它内部维护了Idle、Initialized、Prepared、Started、Paused、Stopped、PlaybackCompleted等一系列状态,每个API只允许在特定状态下调用。比如start()必须在Prepared、Paused或PlaybackCompleted状态下调用,如果你在一个刚new出来还处于Idle状态的对象上直接调用start(),就会抛出IllegalStateException。
最基础的播放流程分为同步和异步两种方式。同步方式调用prepare()会阻塞当前线程,对于本地小文件尚可接受,但对于网络流媒体绝对不能在主线程调用,否则轻则ANR,重则直接崩溃。因此实际开发中几乎都使用异步的prepareAsync(),配合OnPreparedListener回调,在回调中再调用start()开始播放。
MediaPlayer player = new MediaPlayer();
try {
player.setDataSource("https://ipipp.com/media/sample.mp3");
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setOnPreparedListener(mp -> {
// 准备完成,此时才能安全调用start
mp.start();
});
player.setOnCompletionListener(mp -> {
// 播放结束的回调,可以做循环播放或界面刷新
});
player.setOnErrorListener((mp, what, extra) -> {
// 出错时返回true表示自己处理,避免回调继续传播
return true;
});
player.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}这段代码里有几个容易被忽视的点。第一,setDataSource必须在Idle状态下调用,一个MediaPlayer只能设置一次数据源,想换歌就要释放当前对象重新创建。第二,setOnErrorListener的返回值含义不同:返回true表示错误已由应用处理,返回false则会触发OnCompletionListener,这两个回调的关系很多人搞混。第三,所有Listener的回调都在创建MediaPlayer时所在线程的Looper上执行,如果在子线程创建且没有Looper,回调就不会触发,这是很多无声无息不播放问题的根源。
二、播放视频:与SurfaceView和TextureView结合
单独用MediaPlayer只能播放音频,播放视频还需要给它指定一个渲染画布。传统做法是使用SurfaceView,通过setSurface()或者setDisplay()将Surface交给MediaPlayer,解码后的视频帧就会直接绘制到这块独立于View层的Surface上。SurfaceView的优势是渲染效率高,视频绘制在独立的图层,不占用View的绘制通道,即使应用界面复杂也能保持流畅,缺点是它不在View的视图层级内,做旋转、缩放、透明度动画时会比较麻烦。
SurfaceHolder holder = surfaceView.getHolder();
holder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder h) {
try {
player.setDataSource(videoUrl);
player.setSurface(h.getSurface());
player.setOnPreparedListener(MediaPlayer::start);
player.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder h, int format, int w, int height) {
// Surface尺寸变化,MediaPlayer会自动适配,一般无需处理
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder h) {
// Surface销毁前必须解除绑定,否则可能引发底层崩溃
player.setSurface(null);
}
});另一种方案是TextureView,它本身就是普通View,可以自由参与动画和裁剪,适合列表中嵌套视频、视频圆角、视差滚动这类需求。TextureView通过SurfaceTextureListener回调拿到SurfaceTexture,再包装成Surface传给MediaPlayer。它的代价是渲染走GPU合成,比SurfaceView更耗电,在一些低端机上掉帧明显。选型建议很简单:全屏播放、性能敏感的场景用SurfaceView,需要灵活变换和层级叠加的场景用TextureView。
还需要注意生命周期的配合。Activity切到后台时Surface会被销毁,如果此时MediaPlayer还在持有已销毁的Surface继续渲染,就会出现黑屏甚至native层崩溃。正确做法是在onPause或surfaceDestroyed中先调用setSurface(null)解绑,回到前台后再重新绑定,或者直接释放后重建播放器。
三、进度控制、常见报错排查与资源释放
进度控制方面,getCurrentPosition()和getDuration()都是同步方法,可以随时调用获取毫秒值,做进度条时建议用一个定时器每500毫秒刷新一次UI。拖动进度需要用seekTo(int msec),注意这个方法是异步的,调用后并不代表立即跳转完成,可以监听OnSeekCompleteListener确认跳转结束。对于流媒体,某些格式不支持精确seek,实际跳转位置可能和目标位置有偏差,属于正常现象。
// 每500ms刷新一次进度
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (player != null && player.isPlaying()) {
int current = player.getCurrentPosition();
int total = player.getDuration();
seekBar.setProgress(current * 100 / Math.max(total, 1));
handler.postDelayed(this, 500);
}
}
}, 500);
// 用户拖动进度条
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (player != null && player.isPlaying()) {
player.seekTo(seekBar.getProgress());
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { }
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
});常见报错里,出现频率最高的是error(1,-2147483648)和MEDIA_ERROR_SERVER_DIED。前者多是数据源地址无效、网络不通或者音视频编码格式设备不支持,排查时先确认URL在浏览器能否访问,再用ffprobe确认编码格式,Android原生只支持H.264、H.265和常见的AAC、MP3等格式,遇到MKV封装或者FLAC就要考虑用ExoPlayer或ijkplayer。后者表示媒体服务进程挂了,需要调用reset()重置后重新设置数据源。
最后是资源释放,这是最容易被忽略却影响最大的环节。MediaPlayer持有 native 层的解码器和Surface引用,不释放会造成内存泄漏甚至导致后续播放全部失败。正确姿势是在onDestroy中先stop()再release(),并将引用置空。很多人只调用stop()以为就够了,其实stop只是停止播放,native资源依然被占用。另外要养成判空和捕获异常的习惯,把释放逻辑写得足够健壮:
private void releasePlayer() {
if (player != null) {
try {
if (player.isPlaying()) {
player.stop();
}
} catch (IllegalStateException e) {
// 状态异常时忽略,继续释放
} finally {
player.reset();
player.release();
player = null;
}
}
handler.removeCallbacksAndMessages(null);
}总的来说,MediaPlayer适合需求不复杂的场景:播放单个音频文件、简单的视频播放页。如果项目对格式兼容、无缝切换、倍速播放、直播有要求,建议直接上ExoPlayer(现在的Media3),它在这些方面都封装得更好。但在面试和源码学习中,MediaPlayer的状态机设计和异步准备机制依然是绕不开的基础,掌握它对理解Android音视频体系非常有帮助。
Android MediaPlayer音视频播放MediaPlayer状态机修改时间:2026-09-14 13:47:21