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 状态机核心流转 Idle Initialized Preparing Prepared Started Paused Stopped PlaybackCompleted End (release) setDataSource() prepareAsync() onPrepared start() pause() stop() 播放完毕 release() release() release() release()

我的习惯:每次调 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