2. MediaPlayer 深度解析:状态机、异步准备、音频焦点、后台播放与锁屏控制
MediaPlayer 是 Android 音视频开发中最基础、也最容易被用错的组件。说实话,我刚入行那会儿也踩过不少坑——比如在错误的生命周期里调用 prepare(),结果 ANR 了;或者没处理音频焦点,播放器被电话打断后直接崩了。今天咱们就把 MediaPlayer 的这几个核心知识点彻底讲透。
2.1 状态机:MediaPlayer 的“生命线”
MediaPlayer 内部维护了一个严格的状态机。说白了,它就像一台精密仪器,每个按钮只能在特定阶段按下去才有用。你想想看,如果机器还没启动就按“停止”,那肯定报错。
官方文档里定义了 7 种状态:Idle、Initialized、Prepared、Started、Paused、Stopped、PlaybackCompleted。还有两个特殊状态:Error 和 End。我习惯把这 7 个状态画成一张图,方便记忆。
这张图我建议你收藏。每次写 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