2. MediaPlayer 深度解析:状态机、异步准备、音频焦点、后台播放与锁屏控制

MediaPlayer 是 Android 音视频开发中最基础、也最容易被用错的组件。说实话,我刚入行那会儿也踩过不少坑——比如在错误的生命周期里调用 prepare(),结果 ANR 了;或者没处理音频焦点,播放器被电话打断后直接崩了。今天咱们就把 MediaPlayer 的这几个核心知识点彻底讲透。

2.1 状态机:MediaPlayer 的“生命线”

MediaPlayer 内部维护了一个严格的状态机。说白了,它就像一台精密仪器,每个按钮只能在特定阶段按下去才有用。你想想看,如果机器还没启动就按“停止”,那肯定报错。

官方文档里定义了 7 种状态:Idle、Initialized、Prepared、Started、Paused、Stopped、PlaybackCompleted。还有两个特殊状态:Error 和 End。我习惯把这 7 个状态画成一张图,方便记忆。

MediaPlayer 状态机核心流转 Idle Initialized Prepared Started Paused Stopped PlaybackCompleted Error setDataSource() prepare()/prepareAsync() start() pause() stop() start() 播放完成 reset() 回到 Idle

这张图我建议你收藏。每次写 MediaPlayer 代码前,先看一眼状态机,能避免 80% 的运行时异常。

核心原则:任何操作都必须在正确的状态下调用。比如 start() 只能在 Prepared、Started、Paused 状态下调用;stop() 只能在 Prepared、Started、Paused、PlaybackCompleted 状态下调用。

2.2 异步准备:为什么不用 prepare()?

很多新手喜欢直接调 prepare(),觉得简单。但 prepare() 是同步的,它会阻塞主线程直到媒体文件加载完毕。如果文件在网络上,或者是个大文件,主线程一卡就是好几秒,用户早把 App 杀了。

我个人习惯用 prepareAsync(),配合 OnPreparedListener 回调。这样主线程不会被阻塞,用户界面依然流畅。

MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource("https://example.com/audio.mp3");
mediaPlayer.prepareAsync();

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;
    }
});

避坑指南:我曾经在 onPrepared 回调里直接调了 seekTo(),结果发现某些设备上会触发重新准备。后来我改成在 onPrepared 里先 start(),再在播放开始后的第一个位置更新时做 seek,问题就解决了。

2.3 音频焦点:别让你的播放器“打架”

音频焦点是 Android 多音频源管理的核心机制。说白了,就是系统要决定“现在谁的声音该响”。比如你在听歌时突然来了电话,音乐应该暂停;挂断电话后,音乐应该恢复。

我见过不少 App 没处理音频焦点,结果用户打电话时音乐还在后台放,体验极差。处理音频焦点其实不复杂,核心就三步:请求焦点、处理焦点变化、释放焦点。

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

// 1. 请求音频焦点
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();

int focusRequest = audioManager.requestAudioFocus(
        focusChange -> {
            // 2. 处理焦点变化
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_GAIN:
                    // 获得焦点,恢复播放
                    if (!mediaPlayer.isPlaying()) {
                        mediaPlayer.start();
                    }
                    mediaPlayer.setVolume(1.0f, 1.0f);
                    break;
                case AudioManager.AUDIOFOCUS_LOSS:
                    // 永久失去焦点,停止播放
                    if (mediaPlayer.isPlaying()) {
                        mediaPlayer.stop();
                    }
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    // 暂时失去焦点,暂停播放
                    if (mediaPlayer.isPlaying()) {
                        mediaPlayer.pause();
                    }
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    // 可以降低音量继续播放
                    mediaPlayer.setVolume(0.3f, 0.3f);
                    break;
            }
        },
        AudioManager.STREAM_MUSIC,
        AudioManager.AUDIOFOCUS_GAIN
);

// 3. 播放结束后释放焦点
mediaPlayer.setOnCompletionListener(mp -> {
    audioManager.abandonAudioFocus(null);
});

注意:requestAudioFocus 的第三个参数建议用 AUDIOFOCUS_GAIN,而不是 AUDIOFOCUS_GAIN_TRANSIENT。前者表示你要长期占用焦点,后者表示你只是临时用一下。选错了可能导致其他 App 的播放行为异常。

2.4 后台播放:Service 的正确打开方式

Android 从 8.0 开始对后台服务限制越来越严。如果你想让 App 在后台继续播放音乐,必须使用前台服务(Foreground Service),并显示一个通知。

嗯,这里要注意:前台服务不是可选项,而是强制要求。否则你的 App 会在后台运行几分钟后被系统杀掉。

public class MusicService extends Service {
    private MediaPlayer mediaPlayer;
    private static final int NOTIFICATION_ID = 1001;

    @Override
    public void onCreate() {
        super.onCreate();
        mediaPlayer = new MediaPlayer();
        // 初始化 MediaPlayer...
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 启动前台服务
        startForeground(NOTIFICATION_ID, createNotification());
        // 开始播放...
        return START_STICKY;
    }

    private Notification createNotification() {
        // 创建通知渠道(Android 8.0+ 必须)
        NotificationChannel channel = new NotificationChannel(
                "music_channel",
                "音乐播放",
                NotificationManager.IMPORTANCE_LOW
        );
        NotificationManager manager = getSystemService(NotificationManager.class);
        manager.createNotificationChannel(channel);

        // 构建通知
        return new Notification.Builder(this, "music_channel")
                .setContentTitle("正在播放")
                .setContentText("歌曲名称")
                .setSmallIcon(android.R.drawable.ic_media_play)
                .setOngoing(true)
                .build();
    }

    @Override
    public void onDestroy() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
        stopForeground(true);
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

避坑指南:我曾经在 onStartCommand 里返回了 START_NOT_STICKY,结果服务被系统杀掉后没有自动重启,用户切回 App 发现播放器停了。后来我改成 START_STICKY,并在 onStartCommand 里重新初始化 MediaPlayer,问题解决。

2.5 锁屏控制:MediaSession 的妙用

锁屏控制是音乐 App 的标配功能。用户锁屏后,可以通过锁屏界面或通知栏控制播放、暂停、切歌。实现这个功能需要用到 MediaSession。

MediaSession 是 Android 5.0 引入的,它统一了媒体控制的入口。说白了,你只需要注册一个 MediaSession,系统会自动把控制按钮显示在锁屏和通知栏上。

// 创建 MediaSession
MediaSession mediaSession = new MediaSession(this, "MusicSession");
mediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
        MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);

// 设置回调
mediaSession.setCallback(new MediaSession.Callback() {
    @Override
    public void onPlay() {
        mediaPlayer.start();
    }

    @Override
    public void onPause() {
        mediaPlayer.pause();
    }

    @Override
    public void onSkipToNext() {
        // 切到下一首
    }

    @Override
    public void onSkipToPrevious() {
        // 切到上一首
    }

    @Override
    public void onStop() {
        mediaPlayer.stop();
        mediaSession.setActive(false);
    }
});

// 激活 MediaSession
mediaSession.setActive(true);

// 更新播放状态
MediaMetadata metadata = new MediaMetadata.Builder()
        .putString(MediaMetadata.METADATA_KEY_TITLE, "歌曲名称")
        .putString(MediaMetadata.METADATA_KEY_ARTIST, "歌手")
        .build();
mediaSession.setMetadata(metadata);

// 更新播放状态
PlaybackState state = new PlaybackState.Builder()
        .setState(PlaybackState.STATE_PLAYING, 
                  mediaPlayer.getCurrentPosition(), 1.0f)
        .setActions(PlaybackState.ACTION_PLAY | 
                    PlaybackState.ACTION_PAUSE |
                    PlaybackState.ACTION_SKIP_TO_NEXT |
                    PlaybackState.ACTION_SKIP_TO_PREVIOUS)
        .build();
mediaSession.setPlaybackState(state);

关键点:MediaSession 必须和前台服务配合使用。只设置 MediaSession 但不启动前台服务,锁屏控制可能无法正常工作。另外,别忘了在 AndroidManifest.xml 中声明 FOREGROUND_SERVICE 权限。

2.6 综合实战:一个完整的后台播放器

把上面这些知识点串起来,就是一个完整的后台播放器。我整理了一个核心流程表,方便你对照实现。

步骤 操作 关键代码/配置
1 创建前台服务 继承 Service,在 onCreate 中初始化 MediaPlayer
2 请求音频焦点 requestAudioFocus(),处理 AUDIOFOCUS_GAIN/LOSS
3 异步准备 prepareAsync() + OnPreparedListener
4 启动前台服务 startForeground() + 通知
5 注册 MediaSession setCallback() + setActive(true)
6 处理生命周期 onDestroy 中 release()、abandonAudioFocus()

这个流程我用了很多年,基本没出过问题。你只要按这个顺序来,后台播放、锁屏控制、音频焦点都能正常工作。

最后提醒:别忘了在 AndroidManifest.xml 中添加权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

另外,Android 12 以上需要声明前台服务类型:

<service
    android:name=".MusicService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false" />

好了,MediaPlayer 的核心知识点就这些。状态机是基础,异步准备保流畅,音频焦点防冲突,后台服务保存活,MediaSession 做控制。把这五点吃透,你就能写出一个健壮的音乐播放器了。


公众号:蓝海资料掘金营,微信deep3321