MediaPlayer 基础:生命周期、状态机与核心 API

要说 MediaPlayer,其实就是安卓里播放音频视频的那把钥匙。很多人上来就 new 一个对象,然后 start(),结束。嗯,这样确实能响,但坑也多。我自己当年接手过一个视频播放项目,启动时总是卡顿几百毫秒,后来一查,就是没理解状态机,每次都在错误的状态调用方法。说白了,MediaPlayer 的玩意就是一个有限状态机,你推它到哪个态,它才能做哪个事。

生命周期全景

MediaPlayer 的一生可以分成五个大阶段:

  • Idle:刚 new 出来,啥都没干。
  • Initialized:setDataSource() 之后,知道要播啥了。
  • Prepared:prepare() 或 prepareAsync() 完成后,可以开始播。
  • Started / Paused / PlaybackCompleted:播放中的各种子状态。
  • End / Released:release() 之后,对象废了。

你想想看,如果你在 Idle 状态直接调用 start(),那会抛 IllegalStateException。我刚入行时就犯过这错,debug 了半天才发现是状态没切换。所以,理解状态机是第一步。

状态机核心节点与转换

核心结论:每个 API 调用都有它允许的状态范围。Android 官方文档里那张状态图,我建议你打印出来贴在工位上。

下面我用一个 SVG 把主要状态和合法迁移画出来:

MediaPlayer 状态机核心转换图 Idle Initialized Preparing Prepared Started Paused PlaybackCompleted End / Released setDataSource() prepareAsync() onPrepared prepare() start() pause() start() 播放结束自动 seekTo() + start() release() 虚线表示异步流程;实线表示同步调用;playbackCompleted 态可通过 seekTo 重新进入 Started

个人习惯:我一般会在 onPrepared 回调里调用 start(),而不是在 prepareAsync 之后直接 start。这样能保证状态机从 Prepared 进入 Started,避免误操作。

核心 API 速查表

API 合法状态 作用
setDataSource() Idle 设置数据源(文件/URL/资源)
prepare() Initialized 同步准备,阻塞主线程(不推荐 UI 线程用)
prepareAsync() Initialized 异步准备,通过 OnPreparedListener 回调通知
start() Prepared / Started / Paused / PlaybackCompleted 开始或恢复播放
pause() Started 暂停播放
stop() Started / Paused / PlaybackCompleted 停止播放,回到 Prepared 之前的状态
seekTo() Prepared / Started / Paused / PlaybackCompleted 跳转到指定位置(毫秒)
release() 任何非 End 状态 释放资源,进入 End 状态

我曾经踩过的坑:有次我在 Paused 状态调了 stop() 之后,以为还能直接 start(),结果抛异常了。stop 之后你必须先 reset() 再重新 prepare。后来我在每个 API 调用前都加了状态检查,才彻底解决。

生命周期与启动优化关系

好,现在回到我们的主题——预创建 MediaPlayer。为什么要搞预创建?因为 prepare() 或 prepareAsync() 是一个耗时操作。如果你等到用户点击播放按钮才去创建、设置、准备,那延迟妥妥的几百毫秒。我在项目里测过,本地文件 prepareAsync 平均要 50ms,网络流可能上百毫秒。

启动优化的核心思路就是:提前把 MediaPlayer 实例创建好,并提前让它进入 Prepared 状态,放在一个对象池里。用户点了播放,直接从池子里拿一个准备好的实例,调用 start() 就秒开。

说白了,就是用空间换时间。让 MediaPlayer 的状态机提前走到 Prepared,把耗时从用户感知路径里移除。

关键代码骨架

我习惯用一个简单的 MediaPlayer 预创建管理器,伪代码长这样:

public class MediaPlayerPool {
    private BlockingQueue<MediaPlayer> pool = new LinkedBlockingQueue<>(4);

    public MediaPlayer acquire() {
        MediaPlayer mp = pool.poll();
        if (mp == null) {
            mp = createAndPrepare(); // 同步创建,但很少发生
        }
        return mp;
    }

    public void release(MediaPlayer mp) {
        mp.reset();               // 回到 Idle
        mp.setDataSource(url);     // 重新 Initialized
        mp.prepareAsync();         // 进入 Preparing → Prepared
        // 在 OnPreparedListener 中把 mp 放回 pool
    }

    private MediaPlayer createAndPrepare() {
        MediaPlayer mp = new MediaPlayer();
        mp.setDataSource(url);
        mp.prepare();  // 同步准备,在后台线程调用
        return mp;
    }
}

注意:prepare() 不能在 UI 线程调用。我的做法是预创建都在一个后台 HandlerThread 里完成,完成后通过回调通知 UI。

面试时高频问题

  • 问:为什么不能在 Idle 状态调用 prepareAsync?
    答:因为 setDataSource 还没调用,MediaPlayer 不知道要播什么。必须经过 Initialized 状态。
  • 问:预创建的实例用完后怎么处理?
    答:reset() 后重新进入 Idle,再 setDataSource + prepareAsync,放回池子。不能直接 reuse 一个在 Started 状态的实例,会乱。
  • 问:池子大小设多少合适?
    答:一般 2~4 个就够了。太多内存吃不住,太少又不够用。我项目里测过,同时预创建 3 个,启动延迟优化从 500ms 降到 20ms 以下。