
最近在开发一个音乐播放器项目时遇到了一个非常典型的需求如何让一首歌在播放时即使切换到其他页面或应用也能继续播放并且用户可以通过系统通知栏或锁屏界面进行控制。这个需求的核心就是实现“后台播放”与“媒体通知”。在 Android 开发中MediaSession和MediaBrowserService是实现这一功能的标准架构。本文将带你从零开始完整实现一个支持后台播放和媒体通知的音乐播放器涵盖从服务创建、会话管理到通知栏交互的全流程并提供可直接运行的代码示例。1. 背景与核心概念为什么需要 MediaSession在移动应用场景中音频播放是一个常见功能。但简单的MediaPlayer或ExoPlayer实例通常与Activity的生命周期绑定一旦应用退到后台播放就会停止这显然无法满足音乐、播客等应用的需求。MediaSession 框架是 Android 5.0 (API 21) 引入的一套标准化媒体播放控制框架。它解决了几个关键问题后台播放通过Service通常是MediaBrowserService来托管播放逻辑使播放不受 UI 生命周期影响。统一控制接口它将播放器的状态播放/暂停、当前曲目、进度等和控制命令播放、暂停、切歌等抽象成一个会话 (MediaSession)。系统或其他应用如蓝牙设备、智能手表、Android Auto可以通过标准的MediaController与这个会话交互而无需了解播放器内部实现。系统 UI 集成最重要的表现就是锁屏界面和通知栏的媒体控件。系统通过MediaSession获取播放状态并生成可交互的通知。简单来说MediaSession是你的播放器如 ExoPlayer与系统及其他控制方之间的“翻译官”和“经纪人”。2. 环境准备与版本说明本文将基于以下环境进行演示但核心逻辑适用于大多数支持MediaSession的 Android 版本。操作系统: Android 8.0 (API 26) 及以上为了使用MediaSession和NotificationChannel的最佳实践。开发语言: Kotlin最小 SDK:minSdkVersion 21(Android 5.0)这是MediaSession的起始版本。目标 SDK:targetSdkVersion 34(Android 14)主要依赖:AndroidX Media3: Google 官方推荐的现代媒体播放库它集成了ExoPlayer并提供了对MediaSession的顶级支持。我们将使用它来简化开发。AndroidX Core AppCompat: 用于基础 UI 和通知。IDE: Android Studio项目依赖 (app/build.gradle.kts或app/build.gradle):// 在 dependencies 块中添加 dependencies { implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.appcompat:appcompat:1.6.1) // Media3 核心库 implementation(androidx.media3:media3-exoplayer:1.2.1) implementation(androidx.media3:media3-exoplayer-dash:1.2.1) // 如果需要 DASH 流 implementation(androidx.media3:media3-ui:1.2.1) // 播放器 UI 控件 implementation(androidx.media3:media3-session:1.2.1) // MediaSession 支持 implementation(androidx.media3:media3-common:1.2.1) }示例项目结构预览app/ ├── src/main/ │ ├── java/com.example.musicplayer/ │ │ ├── MainActivity.kt # 主界面包含 MediaController │ │ ├── PlayerService.kt # 继承 MediaBrowserService托管播放逻辑 │ │ ├── MusicPlayer.kt # 封装 ExoPlayer 的核心播放器类 │ │ ├── MusicItem.kt # 音乐数据模型 │ │ └── NotificationHelper.kt # 创建媒体通知的工具类 │ └── res/ │ └── ... # 布局、图标等资源3. 核心组件与原理拆解在动手编码前我们需要理解几个核心组件的关系MediaBrowserService: 这是一个特殊的Service它允许外部组件如Activity通过MediaBrowser连接到它并获取一个MediaController来控制播放。我们的播放逻辑就运行在这个服务中。MediaSession: 在MediaBrowserService中创建。它代表一个播放会话维护播放状态PlaybackState和元数据MediaMetadata并处理来自MediaController的控制命令。ExoPlayer (通过 Media3): 实际的音频/视频播放引擎。MediaSession会将接收到的命令如PLAY转发给ExoPlayer执行同时监听ExoPlayer的状态变化来更新MediaSession。MediaController: 在Activity或Fragment中创建。它连接到MediaBrowserService的MediaSession是 UI 层控制播放和监听状态变化的桥梁。MediaNotification: 由系统根据MediaSession的状态自动生成或由我们自定义。它为用户提供了在应用外控制播放的界面。数据流简化视图UI (Activity) --(MediaController)-- MediaSession (在Service内) --(命令)-- ExoPlayer --(状态回调)-- 系统通知栏 --(状态/元数据)-- MediaSession4. 完整实战构建音乐播放器4.1 创建数据模型与播放列表首先定义一个简单的音乐数据模型。// MusicItem.kt package com.example.musicplayer import android.net.Uri import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata data class MusicItem( val id: String, val title: String, val artist: String, val album: String, val durationMs: Long, // 时长毫秒 val mediaUri: Uri, // 音频文件URI可以是本地或网络 val artUri: Uri? null // 专辑封面URI ) { // 转换为 Media3 的 MediaItem便于 ExoPlayer 使用 fun toMediaItem(): MediaItem { return MediaItem.Builder() .setMediaId(id) .setUri(mediaUri) .setMediaMetadata( MediaMetadata.Builder() .setTitle(title) .setArtist(artist) .setAlbumTitle(album) .setArtworkUri(artUri) .build() ) .build() } } // 创建一个简单的播放列表 object MusicPlaylist { val songs listOf( MusicItem( id song_1, title I Cant Stop Me, artist TWICE, album Eyes Wide Open, durationMs 210000, // 3分30秒 mediaUri Uri.parse(asset:///song1.mp3), // 示例假设音频在 assets 文件夹 artUri Uri.parse(https://example.com/cover1.jpg) ), MusicItem( id song_2, title Feel Special, artist TWICE, album Feel Special, durationMs 195000, mediaUri Uri.parse(https://example.com/audio/song2.mp3), artUri Uri.parse(https://example.com/cover2.jpg) ) // ... 可以添加更多歌曲 ) }4.2 实现核心播放器类 (MusicPlayer)这个类封装ExoPlayer并负责将其状态同步到MediaSession。// MusicPlayer.kt package com.example.musicplayer import android.content.Context import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import com.google.common.util.concurrent.MoreExecutors class MusicPlayer(context: Context) { // ExoPlayer 实例 val exoPlayer: ExoPlayer ExoPlayer.Builder(context).build() // Media3 的 MediaSession (需要与 MediaSessionService 配合) private var mediaSession: MediaSession? null // 当前播放列表 private var currentPlaylist: ListMusicItem emptyList() private var currentIndex: Int 0 init { setupPlayerListeners() } private fun setupPlayerListeners() { // 监听播放器状态变化并同步到 MediaSession exoPlayer.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { mediaSession?.let { session - // 将 ExoPlayer 状态映射到 MediaSession 的 PlaybackState val state when (playbackState) { Player.STATE_IDLE - PlaybackStateCompat.STATE_STOPPED Player.STATE_BUFFERING - PlaybackStateCompat.STATE_BUFFERING Player.STATE_READY - { if (exoPlayer.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED } Player.STATE_ENDED - PlaybackStateCompat.STATE_STOPPED else - PlaybackStateCompat.STATE_NONE } // 更新会话的播放状态这里简化实际 Media3 的 MediaSession 会自动处理很多 // 更佳实践是使用 MediaSession.Callback 来关联 } } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { // 歌曲切换时更新 MediaSession 的元数据 updateSessionMetadata() } }) } fun setMediaSession(session: MediaSession) { this.mediaSession session // 将 ExoPlayer 与 MediaSession 关联 session.setPlayer(exoPlayer) } fun preparePlaylist(playlist: ListMusicItem) { currentPlaylist playlist val mediaItems playlist.map { it.toMediaItem() } exoPlayer.setMediaItems(mediaItems) exoPlayer.prepare() updateSessionMetadata() } fun play() { exoPlayer.play() } fun pause() { exoPlayer.pause() } fun playNext() { if (currentPlaylist.isNotEmpty() currentIndex currentPlaylist.size - 1) { currentIndex exoPlayer.seekToDefaultPosition(currentIndex) } else { exoPlayer.seekToNextMediaItem() } } fun playPrevious() { // 简单实现回到上一首开头 if (exoPlayer.currentPosition 3000) { // 如果播放超过3秒则回到当前歌曲开头 exoPlayer.seekTo(0) } else { exoPlayer.seekToPreviousMediaItem() } } fun seekTo(positionMs: Long) { exoPlayer.seekTo(positionMs) } private fun updateSessionMetadata() { // 更新 MediaSession 的元数据如当前播放的歌曲信息 // Media3 的 MediaSession 会自动从关联的 Player 中获取通常无需手动更新 // 这里仅作示意如果需要自定义逻辑可以在这里处理 val currentItem exoPlayer.currentMediaItem // ... 可以基于 currentItem 更新自定义的 MediaSessionCompat 元数据 } fun release() { exoPlayer.release() mediaSession?.release() mediaSession null } }4.3 实现后台播放服务 (PlayerService)这是核心一个MediaSessionService的子类。// PlayerService.kt package com.example.musicplayer import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Intent import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import com.google.common.util.concurrent.MoreExecutors class PlayerService : MediaSessionService() { private lateinit var musicPlayer: MusicPlayer private lateinit var mediaSession: MediaSession companion object { const val NOTIFICATION_CHANNEL_ID music_player_channel const val NOTIFICATION_ID 101 } override fun onCreate() { super.onCreate() createNotificationChannel() initializePlayerAndSession() } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { val channel NotificationChannel( NOTIFICATION_CHANNEL_ID, 音乐播放, NotificationManager.IMPORTANCE_LOW // LOW 级别不会发出声音但会显示在通知栏 ).apply { description 用于控制后台音乐播放 setShowBadge(false) } val manager getSystemService(NotificationManager::class.java) manager.createNotificationChannel(channel) } } private fun initializePlayerAndSession() { musicPlayer MusicPlayer(applicationContext) // 创建 MediaSession关联播放器 mediaSession MediaSession.Builder(this, musicPlayer.exoPlayer) .setCallback(MediaSessionCallback()) .build() // 将 MediaSession 设置给 MusicPlayer用于状态同步如果需要 musicPlayer.setMediaSession(mediaSession) // 准备播放列表 musicPlayer.preparePlaylist(MusicPlaylist.songs) } // 必须重写此方法返回当前的 MediaSession override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { return mediaSession } // 当服务启动时调用我们需要在此处将服务设置为前台服务以防止被系统回收 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 创建并显示前台通知 val notification buildNotification() startForeground(NOTIFICATION_ID, notification) return START_STICKY // 服务被终止后尝试重启 } private fun buildNotification(): Notification { // 这是一个简单示例Media3 提供了 MediaNotification.Provider 来自动生成通知 // 这里为了演示原理手动构建一个基础通知 return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) .setContentTitle(音乐播放器) .setContentText(正在后台播放音乐) .setSmallIcon(android.R.drawable.ic_media_play) // 替换为你的应用图标 .setPriority(NotificationCompat.PRIORITY_LOW) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏可见 .setOngoing(true) // 持续通知 // 注意真正的媒体通知控件播放/暂停按钮需要 MediaStyle 或 Media3 的 MediaNotification // 这里仅作示意完整实现见下文“通知栏优化”部分 .build() } override fun onDestroy() { super.onDestroy() musicPlayer.release() mediaSession.release() } // MediaSession 的回调用于处理来自控制器如通知栏按钮的命令 inner class MediaSessionCallback : MediaSession.Callback { // 可以在这里覆盖命令处理逻辑例如自定义“喜欢”按钮的行为 // 默认情况下命令会直接传递给关联的 Player (ExoPlayer) } }别忘了在AndroidManifest.xml中声明服务和权限!-- AndroidManifest.xml -- manifest ... uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.WAKE_LOCK / !-- 如果需要播放网络音频还需要 INTERNET 权限 -- application ... activity ... !-- 你的主 Activity -- /activity !-- 声明 MediaSessionService -- service android:name.PlayerService android:exportedfalse intent-filter action android:nameandroidx.media3.session.MediaSessionService / /intent-filter /service /application /manifest4.4 实现主界面与控制逻辑 (MainActivity)Activity通过MediaController连接到后台服务。// MainActivity.kt package com.example.musicplayer import android.content.ComponentName import android.os.Bundle import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.MediaControllerCompat import android.widget.Button import android.widget.SeekBar import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { private lateinit var mediaBrowser: MediaBrowserCompat private var mediaController: MediaControllerCompat? null private lateinit var btnPlayPause: Button private lateinit var btnNext: Button private lateinit var btnPrev: Button private lateinit var seekBar: SeekBar private lateinit var tvTitle: TextView private lateinit var tvArtist: TextView private val connectionCallback object : MediaBrowserCompat.ConnectionCallback() { override fun onConnected() { // 连接成功获取 MediaSession 的 Token 并创建 MediaController mediaBrowser.sessionToken.also { token - mediaController MediaControllerCompat(thisMainActivity, token).apply { // 将 Activity 的 MediaController 设置为全局便于系统访问 MediaControllerCompat.setMediaController(thisMainActivity, this) } setupControllerCallback() updateUIFromController() } } override fun onConnectionSuspended() { // 连接暂时断开应尝试重连 } override fun onConnectionFailed() { // 连接失败 } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initViews() // 连接到 MediaBrowserService mediaBrowser MediaBrowserCompat( this, ComponentName(this, PlayerService::class.java), connectionCallback, null // 可选Bundle 参数 ) } override fun onStart() { super.onStart() mediaBrowser.connect() } override fun onStop() { super.onStop() // 断开连接但播放服务仍在后台运行 MediaControllerCompat.getMediaController(this)?.unregisterCallback(controllerCallback) mediaBrowser.disconnect() } private fun initViews() { btnPlayPause findViewById(R.id.btn_play_pause) btnNext findViewById(R.id.btn_next) btnPrev findViewById(R.id.btn_prev) seekBar findViewById(R.id.seek_bar) tvTitle findViewById(R.id.tv_title) tvArtist findViewById(R.id.tv_artist) btnPlayPause.setOnClickListener { val controller mediaController val playbackState controller?.playbackState if (playbackState?.state PlaybackStateCompat.STATE_PLAYING) { controller.transportControls.pause() } else { controller?.transportControls?.play() } } btnNext.setOnClickListener { mediaController?.transportControls?.skipToNext() } btnPrev.setOnClickListener { mediaController?.transportControls?.skipToPrevious() } seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { if (fromUser) { mediaController?.transportControls?.seekTo(progress.toLong()) } } override fun onStartTrackingTouch(seekBar: SeekBar?) {} override fun onStopTrackingTouch(seekBar: SeekBar?) {} }) } private fun setupControllerCallback() { mediaController?.registerCallback(controllerCallback) // 启动一个协程来更新进度条 lifecycleScope.launch { while (true) { delay(1000) // 每秒更新一次 updateProgress() } } } private fun updateProgress() { mediaController?.let { controller - val position controller.playbackState?.position ?: 0L val duration controller.metadata?.getLong(MediaMetadataCompat.METADATA_KEY_DURATION) ?: 0L runOnUiThread { if (duration 0) { seekBar.max duration.toInt() seekBar.progress position.toInt() } } } } private fun updateUIFromController() { mediaController?.let { controller - val metadata controller.metadata val playbackState controller.playbackState runOnUiThread { tvTitle.text metadata?.getString(MediaMetadataCompat.METADATA_KEY_TITLE) ?: 未知标题 tvArtist.text metadata?.getString(MediaMetadataCompat.METADATA_KEY_ARTIST) ?: 未知艺术家 // 更新播放/暂停按钮图标 val isPlaying playbackState?.state PlaybackStateCompat.STATE_PLAYING btnPlayPause.text if (isPlaying) 暂停 else 播放 } } } private val controllerCallback object : MediaControllerCompat.Callback() { override fun onMetadataChanged(metadata: android.support.v4.media.MediaMetadataCompat?) { updateUIFromController() } override fun onPlaybackStateChanged(state: PlaybackStateCompat?) { updateUIFromController() } // 会话被销毁如服务停止 override fun onSessionDestroyed() { // 处理会话结束 } } }4.5 运行与验证启动应用运行应用主界面应显示播放控件。点击播放点击播放按钮音乐应开始播放。此时即使按 Home 键退出应用音乐也应继续。查看通知栏下拉通知栏应该能看到一个带有“音乐播放器”标题的通知。注意我们当前构建的通知还没有媒体控制按钮下一步优化。锁屏控制锁屏后在锁屏界面上应该能看到播放信息取决于手机系统和版本。进程清理测试在开发者选项中打开“不保留活动”或手动划掉应用音乐播放应该继续因为服务是前台服务。5. 优化实现功能完整的媒体通知系统提供的MediaStyle通知可以自动从MediaSession获取信息并生成带控制按钮的通知。在PlayerService中我们可以使用 Media3 库提供的MediaNotification.Provider来简化这一过程。首先更新PlayerService的buildNotification方法或者更佳实践是使用 Media3 的MediaSessionService内置的通知管理。使用 Media3 的默认通知提供器修改PlayerService的onCreate和buildNotification逻辑。实际上MediaSessionService已经内置了与MediaSession关联的通知管理。我们需要做的是配置一个MediaNotification.Provider。一个更完整的实现方式是自定义一个MediaNotification.Provider但为了快速上手我们可以使用默认的MediaNotification.Provider并启动前台服务。// 在 PlayerService 中更新 import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.MediaNotification class PlayerService : MediaSessionService() { // ... 其他代码不变 ... private lateinit var mediaNotification: MediaNotification override fun onCreate() { super.onCreate() createNotificationChannel() initializePlayerAndSession() initializeMediaNotification() } private fun initializeMediaNotification() { // 1. 创建一个 MediaNotification.Provider val notificationProvider DefaultMediaNotificationProvider.Builder(this).build() // 2. 从 MediaSession 构建 MediaNotification mediaNotification notificationProvider.createNotification( mediaSession, MediaNotification.Provider.DEFAULT_NOTIFICATION_ID // 通常使用一个常量ID ) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 使用 MediaNotification 的 notification 作为前台通知 startForeground( mediaNotification.notificationId, mediaNotification.notification ) return START_STICKY } // 当 MediaSession 回调要求更新通知时调用 override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) { if (startInForegroundRequired) { // 如果需要前台服务则更新通知 val updatedNotification mediaNotification.notification // Media3 内部会更新通知内容我们只需要重新调用 startForeground // 注意这里需要根据 Media3 版本调整高版本可能自动处理 // 一种简单方式是重新创建 MediaNotification initializeMediaNotification() startForeground(mediaNotification.notificationId, mediaNotification.notification) } else { // 如果不需要前台则更新通知即可 notificationManager.notify(mediaNotification.notificationId, mediaNotification.notification) } } // ... onDestroy 等其他方法 ... }关键点DefaultMediaNotificationProvider会根据MediaSession的播放状态和元数据自动生成一个符合 Android 设计规范的媒体通知包含专辑图、歌曲名、艺术家、播放/暂停、上一首/下一首按钮并且这些按钮的控制命令会自动发送到你的MediaSession。6. 常见问题与排查思路在实现后台播放和媒体通知时开发者常会遇到以下问题问题现象可能原因排查步骤与解决方案应用退到后台后播放立即停止1. 播放器在Activity中创建生命周期绑定。2.Service未正确设置为前台服务被系统回收。1. 确保播放逻辑在Service如MediaBrowserService中。2. 在Service的onStartCommand中调用startForeground并提供一个持续的通知。通知栏没有显示或没有控制按钮1. 未创建通知渠道Android 8.0。2. 通知未使用MediaStyle或未与MediaSession关联。3. 通知的priority或visibility设置不正确。1. 在Service启动时创建NotificationChannel。2. 使用Media3的DefaultMediaNotificationProvider或androidx.media.app.NotificationCompat.MediaStyle来构建通知。3. 确保通知设置了setVisibility(NotificationCompat.VISIBILITY_PUBLIC)。锁屏界面不显示播放控件1. 手机系统或锁屏设置禁用了媒体控件。2. 通知的可见性未设置为VISIBILITY_PUBLIC。3.MediaSession的PlaybackState和Metadata未正确更新。1. 检查系统设置。2. 确认通知构建时调用了setVisibility(NotificationCompat.VISIBILITY_PUBLIC)。3. 确保MediaSession.setCallback已设置并且播放器状态变化时通过MediaSessionCompat.Callback更新了setPlaybackState和setMetadata。点击通知栏按钮无反应1.PendingIntent未正确关联到MediaSession。2.MediaSession.Callback未正确实现或未处理相应动作。1. 如果使用MediaStyle确保在setMediaSession方法中传入了MediaSessionCompat.Token。2. 检查MediaSession.Callback的onPlay,onPause等方法是否被调用并正确转发给了播放器。连接 Service 失败1.AndroidManifest.xml中未正确声明Service或intent-filter。2.MediaBrowser连接的ComponentName不正确。1. 检查AndroidManifest.xml确保Service声明中包含action android:nameandroidx.media3.session.MediaSessionService /。2. 确认ComponentName的参数是PlayerService::class.java。播放网络音频失败1. 未申请INTERNET权限。2. 网络链接问题或音频链接失效。3.ExoPlayer未添加相应的数据源扩展如media3-exoplayer-hls用于 HLS 流。1. 在AndroidManifest.xml中添加uses-permission android:nameandroid.permission.INTERNET /。2. 检查音频 URL 是否有效。3. 根据音频格式添加对应的media3-exoplayer-*依赖。7. 最佳实践与工程建议实现一个健壮的后台音乐播放器除了核心功能还需要考虑以下工程化细节生命周期管理服务启动策略考虑使用startService还是bindService。通常音乐播放器采用“启动并绑定”模式确保没有 UI 绑定时服务也能运行。进程死亡恢复在onStartCommand中返回START_STICKY使服务被系统杀死后能重启。重启后应尝试恢复之前的播放状态播放位置、播放列表等这需要将状态持久化到SharedPreferences或数据库中。音频焦点管理当其他应用如电话、导航需要播放声音时你的应用应该暂停播放。使用AudioManager.requestAudioFocus来获取音频焦点并在OnAudioFocusChangeListener中根据焦点变化调整播放行为如短暂失去焦点时降低音量永久失去焦点时暂停。耳机和蓝牙控制MediaSession框架已经很好地支持了蓝牙设备的多媒体按键。确保你的MediaSession.Callback正确响应ACTION_PLAY,ACTION_PAUSE,ACTION_SKIP_TO_NEXT,ACTION_SKIP_TO_PREVIOUS等标准命令。监听耳机插拔事件拔掉耳机时自动暂停播放。通知栏优化自定义通知布局如果默认样式不满足需求可以自定义通知布局但务必通过MediaSession来更新播放状态保证系统控件的一致性。添加自定义动作可以在通知上添加“喜欢”、“收藏”等自定义按钮通过在构建通知时添加PendingIntent来实现并在BroadcastReceiver或Service中处理。错误处理与重试ExoPlayer提供了丰富的监听器Player.Listener要监听onPlayerError事件并根据错误类型如网络错误、解码错误给用户友好的提示或自动重试。电量优化使用WakeLock时要谨慎只在播放时持有暂停或停止时释放。考虑使用WorkManager或JobScheduler来管理非即时性的任务如下载播放列表。测试后台播放测试反复切换应用、锁屏、清理后台进程验证播放是否持续。通知栏测试验证所有按钮功能是否正常。音频焦点测试播放音乐时接打电话或启动其他音乐应用检查行为是否符合预期。设备兼容性测试在不同品牌、不同 Android 版本的设备上测试特别是通知栏和锁屏控件的显示。通过本文的步骤你已经构建了一个具备后台播放和媒体通知基础功能的音乐播放器。核心在于理解MediaSession作为中枢的连接作用以及Service与UI分离的架构。接下来你可以在此基础上添加播放列表管理、歌词显示、音效均衡器等更复杂的功能。