2. MediaPlayer基础:生命周期、状态机与基本使用流程
MediaPlayer,说白了就是Android官方给我们封装好的一个多媒体播放器。你想想看,如果让你自己从零去解码音频、控制播放、管理缓冲区,那得写多少代码?好在Google帮我们搞定了这些脏活累活。
我个人习惯把MediaPlayer理解成一个"黑盒子"——你给它一个文件路径,它就能给你放出声音来。但如果你不了解它的内部状态,很容易写出各种奇怪的bug。我在项目中就遇到过好几次,明明调了start(),播放器却毫无反应,后来才发现是状态不对。
2.1 MediaPlayer的生命周期
MediaPlayer的生命周期,说白了就是它从创建到销毁的整个过程。跟Activity的生命周期有点像,但更复杂一些。它不是一个简单的"创建-使用-销毁"三步走,而是有一套完整的状态机在背后控制。
嗯,这里要注意:MediaPlayer的生命周期管理,核心就是状态管理。你调用的每个方法,都必须在正确的状态下进行。否则,轻则方法无效,重则直接抛异常。
核心要点:MediaPlayer的状态转换是单向的,不能随意跳转。比如你不能从"播放中"直接跳到"初始化完成",必须先暂停、再停止、再重置。
2.2 状态机详解
MediaPlayer官方文档里定义了一套完整的状态机,总共包含以下几个状态:
| 状态 | 说明 | 可调用的主要方法 |
|---|---|---|
| Idle(空闲) | 刚创建或调用reset()后的状态 | setDataSource() |
| Initialized(初始化) | 调用setDataSource()后 | prepare() / prepareAsync() |
| Prepared(准备就绪) | prepare()完成后 | start()、seekTo()、getDuration() |
| Started(播放中) | 调用start()后 | pause()、stop()、seekTo() |
| Paused(暂停) | 调用pause()后 | start()、stop()、seekTo() |
| Stopped(停止) | 调用stop()后 | prepare() / prepareAsync()、start() |
| PlaybackCompleted(播放完成) | 播放到文件末尾 | start()(重新播放)、stop() |
| End(结束) | 调用release()后 | 无 |
为什么会设计这么多状态?我刚开始学的时候也觉得麻烦。后来在做一个音乐播放器项目时,用户频繁切换歌曲、暂停、快进,我才意识到状态机的重要性——它保证了播放器在任何时刻的行为都是可预测的。
我的经验:写代码时,建议在每次调用MediaPlayer方法前,先检查当前状态。可以用一个成员变量记录状态,或者直接try-catch。我曾经因为忘记检查状态,在播放完成后直接调了stop(),结果抛了个IllegalStateException,用户直接闪退。
2.3 核心流程图
下面这张图,是我自己整理的MediaPlayer状态机流转图。你把它保存下来,写代码时对照着看,基本不会出错。
看图技巧:绿色背景的节点是"可播放"状态,红色背景是"停止"状态,灰色是"已释放"状态。虚线箭头表示release()可以从任意状态直接跳到End。
2.4 基本使用流程
好了,理论说完了,咱们直接上代码。下面是一个最基础的MediaPlayer使用流程:
// 1. 创建MediaPlayer实例
MediaPlayer mediaPlayer = new MediaPlayer();
try {
// 2. 设置数据源(可以是本地文件、网络URL、ContentProvider等)
mediaPlayer.setDataSource("https://example.com/audio.mp3");
// 3. 准备播放(同步方式,会阻塞主线程)
// 建议用prepareAsync()配合监听器,避免ANR
mediaPlayer.prepare();
// 4. 开始播放
mediaPlayer.start();
// 5. 暂停播放
// mediaPlayer.pause();
// 6. 停止播放
// mediaPlayer.stop();
// 7. 释放资源
// mediaPlayer.release();
} catch (IOException e) {
e.printStackTrace();
}
这段代码看起来很简单,对吧?但实际项目中,你不可能这么写。为什么?因为prepare()是同步的,如果网络不好,它可能会卡住主线程好几秒,直接导致ANR。
避坑指南:我曾经在一个直播项目中,直接用prepare()加载一个远程音频流。结果用户反馈播放器经常卡死。查了半天才发现是网络波动导致prepare()阻塞了主线程。后来全部改成prepareAsync(),问题就解决了。
2.5 异步准备与监听器
正确的做法是使用prepareAsync(),然后通过监听器来获取准备结果:
MediaPlayer mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource("https://example.com/audio.mp3");
// 异步准备,不阻塞主线程
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// 准备完成,可以开始播放了
mp.start();
}
});
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 处理错误,比如网络超时、文件损坏等
Log.e("MediaPlayer", "Error: " + what + ", " + extra);
return true; // 表示已处理错误
}
});
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
你想想看,这样写的好处是什么?主线程不会被阻塞,用户界面依然可以流畅操作。而且通过监听器,我们可以精确地知道播放器当前的状态。
2.6 完整的使用模板
下面是我个人在项目中常用的一个模板,包含了完整的生命周期管理:
public class AudioPlayer {
private MediaPlayer mediaPlayer;
private boolean isPrepared = false;
public void play(String url) {
// 如果已经存在播放器,先释放
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(url);
mediaPlayer.setOnPreparedListener(mp -> {
isPrepared = true;
mp.start();
});
mediaPlayer.setOnCompletionListener(mp -> {
// 播放完成,重置状态
isPrepared = false;
});
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
isPrepared = false;
return true;
});
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
public void pause() {
if (mediaPlayer != null && isPrepared && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
public void resume() {
if (mediaPlayer != null && isPrepared && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
}
public void stop() {
if (mediaPlayer != null && isPrepared) {
mediaPlayer.stop();
isPrepared = false;
}
}
public void release() {
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
isPrepared = false;
}
}
}
核心要点:每次调用MediaPlayer的方法前,都要检查当前状态。我习惯用一个isPrepared标志位来记录准备状态,这样可以避免很多不必要的异常。
2.7 常见问题与避坑
最后,分享几个我在项目中踩过的坑:
- 重复调用prepare():MediaPlayer只能prepare一次,第二次会抛异常。如果需要重新播放,先调用reset()回到Idle状态。
- 忘记释放资源:MediaPlayer持有底层解码器和音频硬件的资源,不释放会导致内存泄漏。一定要在Activity的onDestroy()或Fragment的onDestroyView()中调用release()。
- 在子线程操作MediaPlayer:MediaPlayer不是线程安全的,所有操作都应该在主线程进行。如果需要在子线程中控制播放,用Handler切换到主线程。
- 音频焦点:播放音频时,要处理音频焦点变化。比如来电话时,你的播放器应该暂停。这个后面会详细讲。
嗯,MediaPlayer的基础就这些了。说白了,它就是一套状态机驱动的播放器框架。你只要掌握了状态转换的规律,写出来的代码就会很稳。下一节我们会深入讲MediaPlayer的高级用法,包括如何自定义播放控制、如何处理音频焦点等。
公众号:蓝海资料掘金营,微信deep3321