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状态机流转图。你把它保存下来,写代码时对照着看,基本不会出错。

Idle(空闲) Initialized(初始化) Prepared(就绪) Started(播放中) Paused(暂停) Stopped(停止) PlaybackCompleted(完成) End(释放) setDataSource() prepare() start() pause() start() stop() stop() prepare() 播放结束 start() release() 图例 可播放状态 停止/错误状态 已释放状态 中间状态

看图技巧:绿色背景的节点是"可播放"状态,红色背景是"停止"状态,灰色是"已释放"状态。虚线箭头表示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