16、MediaPlayer深入:状态机、内部架构与自定义播放器封装
说实话,MediaPlayer 是 Android 音视频开发里最基础也最绕不开的一个组件。我早年刚接触它的时候,觉得不就是个播放器嘛,new 出来调个 start 就完事了。结果呢?项目上线后各种崩溃、状态错乱、资源泄漏……嗯,后来我才明白,不搞懂它的状态机,你根本驾驭不了它。
今天我们就来彻底扒一扒 MediaPlayer 的底裤。我会从状态机讲起,再到内部架构,最后带你封装一个靠谱的自定义播放器。
16.1 MediaPlayer 状态机:别乱调 API
MediaPlayer 内部维护了一套严格的状态机。你调用的每个方法,都只能在特定状态下执行。比如你还没 prepare 就调 start,直接抛异常。
官方文档里那张状态图,我建议你打印出来贴工位上。我自己当年就是没当回事,结果线上出了个诡异 bug:用户快速切歌时,MediaPlayer 还在 Preparing 状态,我又调了一次 reset,直接 IllegalStateException。
核心状态一览:
- Idle:刚 new 出来,或者调了 reset() 之后
- Initialized:调了 setDataSource() 之后
- Preparing / Prepared:异步/同步准备中或完成
- Started:正在播放
- Paused:暂停
- Stopped:停止
- PlaybackCompleted:播放完毕
- End:调了 release() 之后,彻底销毁
我习惯用一张图来理解它,你看下面这个 SVG,基本把状态流转画清楚了:
我的习惯:每次调 MediaPlayer 的方法前,先判断当前状态。我一般用一个枚举变量自己维护状态,而不是完全依赖 MediaPlayer 内部状态。为什么?因为有些异步回调的时序问题,你光靠监听是不够的。
16.2 内部架构:MediaPlayer 到底怎么工作的?
MediaPlayer 不是一个人在战斗。它背后是一整套 C++ 框架,叫 NuPlayer(Android 5.0 之后默认)。你想想看,Java 层的 MediaPlayer 只是个壳,真正干活的是 Native 层的 NuPlayer。
我简单梳理一下它的分层:
| 层级 | 组件 | 职责 |
|---|---|---|
| Java 层 | MediaPlayer.java | 对外 API,状态机管理,回调分发 |
| JNI 层 | android_media_MediaPlayer.cpp | Java 与 Native 的桥梁 |
| Native 层 | NuPlayer / AwesomePlayer | 核心解码、渲染、同步逻辑 |
| 解码层 | MediaCodec / Stagefright | 硬解/软解,音视频分离 |
| 输出层 | AudioTrack / Surface | 音频输出到扬声器,视频输出到 Surface |
说白了,你调一个 start(),Java 层通过 JNI 通知 Native 层,Native 层从解码器拉数据,然后分别送给 AudioTrack 和 Surface。整个过程是异步流水线式的。
我曾经踩过的坑:在低端机上,MediaPlayer 的 onPrepared 回调可能延迟好几秒。如果你在回调里直接调 start(),用户会感觉点击播放后没反应。我当时的解决方案是:在 UI 层先显示一个加载动画,等真正开始播放了再隐藏。
16.3 自定义播放器封装:别再裸写 MediaPlayer 了
直接在你的 Activity 或 Fragment 里写 MediaPlayer 逻辑,那是在给自己挖坑。我见过太多项目,一个页面里 MediaPlayer 的创建、销毁、状态判断散落在各处,维护起来简直想哭。
我建议你封装一个 MediaPlayerWrapper,把状态管理、生命周期、错误处理都收拢到一个类里。下面是我常用的模板:
public class MediaPlayerWrapper {
private MediaPlayer mediaPlayer;
private PlayerState currentState = PlayerState.IDLE;
public enum PlayerState {
IDLE, INITIALIZED, PREPARING, PREPARED,
STARTED, PAUSED, STOPPED, COMPLETED, ERROR, RELEASED
}
public MediaPlayerWrapper() {
mediaPlayer = new MediaPlayer();
currentState = PlayerState.IDLE;
setupListeners();
}
private void setupListeners() {
mediaPlayer.setOnPreparedListener(mp -> {
currentState = PlayerState.PREPARED;
// 通知外部
});
mediaPlayer.setOnCompletionListener(mp -> {
currentState = PlayerState.COMPLETED;
});
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
currentState = PlayerState.ERROR;
// 释放资源,尝试重试
return true;
});
}
public void setDataSource(String path) throws IOException {
if (currentState != PlayerState.IDLE) {
throw new IllegalStateException("只能在 IDLE 状态设置数据源");
}
mediaPlayer.setDataSource(path);
currentState = PlayerState.INITIALIZED;
}
public void prepareAsync() {
if (currentState != PlayerState.INITIALIZED) {
throw new IllegalStateException("只能在 INITIALIZED 状态 prepare");
}
currentState = PlayerState.PREPARING;
mediaPlayer.prepareAsync();
}
public void start() {
if (currentState != PlayerState.PREPARED && currentState != PlayerState.PAUSED) {
throw new IllegalStateException("只能在 PREPARED 或 PAUSED 状态 start");
}
mediaPlayer.start();
currentState = PlayerState.STARTED;
}
public void pause() {
if (currentState != PlayerState.STARTED) {
throw new IllegalStateException("只能在 STARTED 状态 pause");
}
mediaPlayer.pause();
currentState = PlayerState.PAUSED;
}
public void stop() {
if (currentState == PlayerState.STARTED || currentState == PlayerState.PAUSED) {
mediaPlayer.stop();
currentState = PlayerState.STOPPED;
}
}
public void release() {
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
currentState = PlayerState.RELEASED;
}
}
public PlayerState getCurrentState() {
return currentState;
}
}
封装的好处:
- 所有状态切换都在一个类里,出问题好排查
- 外部调用者不需要关心 MediaPlayer 的细节
- 方便做单元测试,你可以 mock 这个 Wrapper
- 后续如果要切换到 ExoPlayer,只需要改这个类的内部实现
16.4 生命周期绑定:别让播放器泄漏
Activity 或 Fragment 销毁时,MediaPlayer 必须释放。我见过有人只在 onDestroy 里调 release,但如果是屏幕旋转呢?Activity 重建了,旧的 MediaPlayer 还活着,新的又创建了一个,内存泄漏妥妥的。
我的做法是:在 onPause() 里暂停播放,在 onStop() 里释放资源,在 onResume() 里重新创建。当然,具体策略要看你的业务场景。比如视频通话类的应用,可能就不适合在 onPause 里暂停。
核心原则:
- 谁创建,谁释放
- 生命周期回调里,不要做耗时操作
- 异步回调中,要判断宿主是否还存活
- 使用 WeakReference 持有 Activity 或 Context
16.5 错误处理与重试机制
MediaPlayer 的 onError 回调里,what 和 extra 参数能告诉你具体是什么错误。比如 MEDIA_ERROR_SERVER_DIED 表示 mediaserver 挂了,这时候你需要完全重建 MediaPlayer。
我一般这样处理:
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
Log.e("MediaPlayerWrapper", "错误码: " + what + ", 额外信息: " + extra);
if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
// mediaserver 挂了,必须重建
release();
// 重新初始化
initPlayer();
// 尝试重新播放
play(url);
} else {
// 其他错误,比如网络超时,可以尝试重试
retryCount++;
if (retryCount < MAX_RETRY) {
play(url);
} else {
// 通知用户
}
}
return true; // 表示我们已经处理了错误
});
注意:onError 回调里不要直接调 release,因为此时 MediaPlayer 内部可能还在处理某些事务。我习惯 post 到主线程的消息队列里,延迟 100ms 再执行释放操作。
16.6 总结
MediaPlayer 看似简单,但用好了不容易。状态机是它的命门,内部架构是它的骨架,封装是让它变得可控的手段。我个人建议,新项目直接上 ExoPlayer,但如果你维护的是老项目,或者需要极简的播放功能,MediaPlayer 依然是个好选择。
记住一句话:永远不要直接操作 MediaPlayer 的原始 API,封装一层,你会感谢自己的。
公众号:蓝海资料掘金营,微信deep3321