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 把主要状态和合法迁移画出来:
个人习惯:我一般会在 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 以下。