一、项目介绍
1.1 背景与意义
在音乐或音频类应用中,用户往往希望在切换到其他应用,甚至按下 Home 键时,音乐仍能持续播放。Android 默认情况下,当应用退到后台后,Activity 的生命周期会被暂停或销毁,如果不做特殊处理,MediaPlayer 会随之停止播放。为确保用户体验,需要将播放逻辑放到后台可持续运行的组件——Service 中,并结合前台服务(Foreground Service)和Notification,保证在系统资源紧张时仍能保活。
典型场景包括:
-
音乐播放器 App:用户在听歌时切换到微信或浏览器,不希望音乐中断。
-
播客/有声读物:听小说或访谈,放到后台后继续收听;
-
导航或直播:语音播报或直播音频退到后台也要持续播放。
1.2 项目目标
-
构建一个可在后台持续播放音乐的 Demo;
-
播放逻辑放在
Service
中,并以前台服务形式运行; -
在通知栏显示播放进度、控制按钮(播放/暂停、停止);
-
支持在 Activity 中一键启动/停止服务;
-
所有代码整合到一个代码块,通过注释区分不同文件,注释要非常详细;
-
拓展相关知识:Android 后台限制、前台服务、Notification Channel、Audio Focus 处理等。
二、相关知识拓展
-
Service 与前台服务
-
普通
Service
在系统资源紧张时可能被系统回收; -
前台服务(调用
startForeground()
)绑定一个常驻通知,能显著降低被回收风险,并且适用于播放、导航、Step Counter 等持续任务。
-
-
Android 8.0+ 后台执行限制
-
自 API 26 起,后台启动 Service 受到限制,需要使用
startForegroundService()
启动,Service 在 5 秒内必须调用startForeground()
;
-
-
Notification Channel
-
自 API 26 起,创建通知前必须先注册渠道(Channel),否则通知不会显示;
-
-
MediaPlayer 与 Audio Focus
-
正确获取并监听
AudioManager
的音频焦点,处理来电、中断、多媒体同时播放等场景;
-
-
MediaSession & MediaStyle Notification(可扩展)
-
使用
MediaSession
对外暴露控制接口,在锁屏、蓝牙设备、Android Auto 上也能使用通知栏媒体控件。
-
三、实现思路
-
在
AndroidManifest.xml
-
申请
FOREGROUND_SERVICE
权限; -
声明
MusicService
。
-
-
编写
MusicService
-
继承自
Service
,持有MediaPlayer
实例; -
在
onStartCommand()
中初始化并开始播放; -
调用
startForeground()
并创建一个带播放控制按钮的通知; -
通过
Notification
的 Action 点击 PendingIntent 来控制播放/暂停与停止。
-
-
编写
MainActivity
-
提供两个按钮:开始播放(启动前台服务)与停止播放(停止服务);
-
在
onDestroy()
中可解除绑定或停止服务。
-
-
Notification 与交互
-
建立 Notification Channel;
-
构建
MediaStyle
通知,添加“暂停/播放”、“停止”按钮; -
在 Service 中接收 Intent Action 并执行对应命令。
-
-
处理音频焦点(可选)
-
在 Service 中向
AudioManager
请求焦点,并监听焦点变化,做“暂退”或“停止”处理。
-
四、完整代码整合(含详细注释)
// ==================== File: AndroidManifest.xml ====================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.backgroundmusic">
<!-- 8.0+ 启动前台服务需声明该权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="后台音乐播放"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- 活动入口 -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 后台播放服务 -->
<service
android:name=".MusicService"
android:exported="false"/>
</application>
</manifest>
// ==================== File: MainActivity.java ====================
package com.example.backgroundmusic;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
/**
* MainActivity:提供启动、停止后台音乐播放的按钮
*/
public class MainActivity extends AppCompatActivity {
private Button btnStart, btnStop;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 加载布局
btnStart = findViewById(R.id.btn_start);
btnStop = findViewById(R.id.btn_stop);
// 点击“开始播放”,启动前台服务
btnStart.setOnClickListener(v -> {
Intent intent = new Intent(this, MusicService.class);
// API26+ 要用 startForegroundService
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
});
// 点击“停止播放”,停止服务
btnStop.setOnClickListener(v -> {
Intent intent = new Intent(this, MusicService.class);
stopService(intent);
});
}
}
// ==================== File: activity_main.xml ====================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始播放音乐"/>
<Button
android:id="@+id/btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="停止播放音乐"
android:layout_marginTop="16dp"/>
</LinearLayout>
// ==================== File: MusicService.java ====================
package com.example.backgroundmusic;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
/**
* MusicService:在前台服务中使用 MediaPlayer 播放音乐
*/
public class MusicService extends Service {
// 通知通道与通知 ID
private static final String CHANNEL_ID = "music_play_channel";
private static final int NOTIF_ID = 1001;
// 通知动作字符串,用于区分点击事件
private static final String ACTION_PLAY = "ACTION_PLAY";
private static final String ACTION_PAUSE = "ACTION_PAUSE";
private static final String ACTION_STOP = "ACTION_STOP";
private MediaPlayer mediaPlayer;
private boolean isPlaying = false;
@Override
public void onCreate() {
super.onCreate();
// 1. 创建通知渠道(Android 8.0+)
createNotificationChannel();
// 2. 初始化 MediaPlayer
initMediaPlayer();
}
/**
* 在 startService/startForegroundService 后回调
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 区分点击通知的 Action
if (intent != null && intent.getAction() != null) {
switch (intent.getAction()) {
case ACTION_PLAY:
resumeMusic();
break;
case ACTION_PAUSE:
pauseMusic();
break;
case ACTION_STOP:
stopSelf(); // 停止 Service
return START_NOT_STICKY;
}
} else {
// 首次启动,直接开始播放
startMusic();
}
// 每次 onStartCommand 都需要调用 startForeground,保持前台状态
startForeground(NOTIF_ID, buildNotification());
// 如果被系统杀掉,不再自动重启
return START_NOT_STICKY;
}
/**
* 初始化 MediaPlayer,并设置音频属性
*/
private void initMediaPlayer() {
mediaPlayer = MediaPlayer.create(this, R.raw.sample_music);
mediaPlayer.setLooping(true); // 循环播放
// 适配 API21+ 的音频属性
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mediaPlayer.setAudioAttributes(
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
);
} else {
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
}
/** 开始播放并更新状态 */
private void startMusic() {
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
isPlaying = true;
}
}
/** 暂停播放并更新状态 */
private void pauseMusic() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
isPlaying = false;
}
}
/** 恢复播放并更新状态 */
private void resumeMusic() {
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
isPlaying = true;
}
}
/** 构建前台服务通知,包含播放/暂停/停止按钮 */
private Notification buildNotification() {
// 点击通知主体,返回 MainActivity
PendingIntent mainIntent = PendingIntent.getActivity(
this, 0,
new Intent(this, MainActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 播放/暂停按钮 Intent
Intent playPauseIntent = new Intent(this, MusicService.class)
.setAction(isPlaying ? ACTION_PAUSE : ACTION_PLAY);
PendingIntent ppPending = PendingIntent.getService(
this, 1, playPauseIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 停止按钮 Intent
Intent stopIntent = new Intent(this, MusicService.class)
.setAction(ACTION_STOP);
PendingIntent stopPending = PendingIntent.getService(
this, 2, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 构造 Notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在播放音乐")
.setContentText(isPlaying ? "点击暂停" : "点击播放")
.setSmallIcon(R.drawable.ic_music_note)
.setContentIntent(mainIntent) // 点击通知主体
.addAction(isPlaying ? R.drawable.ic_pause : R.drawable.ic_play,
isPlaying ? "暂停" : "播放", ppPending)
.addAction(R.drawable.ic_stop, "停止", stopPending)
// 使用 MediaStyle,支持在锁屏及车载展示
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1));
return builder.build();
}
/** 创建通知渠道(Android 8.0+) */
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel chan = new NotificationChannel(
CHANNEL_ID,
"音乐播放服务",
NotificationManager.IMPORTANCE_LOW
);
chan.setDescription("用于在后台播放音乐的前台服务");
NotificationManager mgr = getSystemService(NotificationManager.class);
if (mgr != null) {
mgr.createNotificationChannel(chan);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
// 释放 MediaPlayer
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
// 取消前台状态与通知
stopForeground(true);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
// 不提供绑定,此处返回 null
return null;
}
}
说明: 需在
res/raw/
目录下添加一个sample_music.mp3
作为示例音乐文件,并在res/drawable/
下提供ic_music_note
、ic_play
、ic_pause
、ic_stop
等图标。
五、代码解读(方法作用说明)
-
AndroidManifest.xml
-
声明
FOREGROUND_SERVICE
权限; -
注册
MusicService
,使系统识别该 Service。
-
-
MainActivity
-
btnStart
点击调用startForegroundService()
(API26+)或startService()
; -
btnStop
点击调用stopService()
停止播放。
-
-
MusicService.onCreate()
-
调用
createNotificationChannel()
为通知创建渠道; -
调用
initMediaPlayer()
初始化MediaPlayer
,并设置音频属性与循环模式。
-
-
MusicService.onStartCommand()`
-
根据 Intent 的
getAction()
判断是“首次启动”“播放/暂停”还是“停止”; -
调用相应的
startMusic()
、pauseMusic()
或stopSelf()
; -
每次都需调用
startForeground()
并传入最新状态的通知。
-
-
buildNotification()
-
构建跳回主界面与播放/暂停/停止的
PendingIntent
; -
使用
NotificationCompat.MediaStyle()
,将控制按钮在紧凑视图中展示; -
setContentIntent()
让用户点击通知主体可返回应用。
-
-
onDestroy()
-
停止并释放
MediaPlayer
; -
调用
stopForeground(true)
取消前台服务并移除通知。
-
六、项目总结与拓展
6.1 实现效果回顾
-
将播放逻辑放入
Service
,并以前台服务形式运行,确保应用退到后台时音乐不被系统回收; -
通知栏提供播放/暂停/停止按钮,用户可直接从通知控制;
-
兼容 Android 8.0+ 的后台执行限制与通知渠道要求。
6.2 常见坑与注意
-
前台服务未及时调用
startForeground
:API26+ 调用startForegroundService()
后,Service 必须在 5 秒内调用startForeground()
,否则系统会杀掉 Service; -
缺少通知渠道:Android8.0+ 若不创建通知渠道,通知无法显示;
-
PendingIntent FLAG:使用
FLAG_IMMUTABLE
或FLAG_MUTABLE
以满足 Android12+ 的安全要求; -
音频焦点管理:当前未处理来电或其他应用夺取焦点时的暂停逻辑,生产环境需注册
AudioManager.OnAudioFocusChangeListener
;
6.3 可扩展方向
-
MediaSession & MediaStyle 深度集成:让通知支持更多控制按钮,并与蓝牙、锁屏、Android Auto 等设备互通;
-
播放队列与进度控制:在通知上展示进度条,并能拖动进度;
-
使用 ExoPlayer:替代
MediaPlayer
,更强大的缓存与格式兼容能力; -
音频焦点 & 媒体按钮:监听耳机按键、来电状态,实现更好的用户体验;
-
歌词展示 &可视化:结合
Visualizer
API,实现歌词滚动或音频频谱可视化。