Android原生MediaPlayer实现生产级本地音乐播放器 简介这是一份面向Android初学者与进阶开发者的完整音乐播放器实战项目基于Android Studio开发环境覆盖多媒体播放、UI交互、用户账户管理、数据持久化及权限配置等核心知识点助力开发者系统掌握Android应用开发全流程。资源包共712个文件包含98个XML布局文件定义界面结构、161个Flat资源含图标与样式、122个JSON配置如歌曲元数据或用户信息、39个DEX字节码及37个Class类文件另有16个Java源码文件构成主业务逻辑整体压缩包大小为44.15MB。已有7398人学习下载说明其在实践教学与自学训练中广受认可。读者可直接导入Android Studio运行调试获得可执行的完整工程含MediaPlayer基础控制、上下曲切换逻辑、SQLite本地注册登录模块、背景图动态更换实现以及Gradle构建配置、权限声明与基础网络请求示例是理解Android组件协同与工程规范的优质入门范例。1. 一个能真正在 Android 设备上播放本地音乐、支持后台续播、带进度拖拽和通知栏控制的播放器不是 Demo而是可直接集成进真实项目的最小可行实现很多人在 Android Studio 里新建一个空 Activity往res/layout里拖个Button和TextView就以为自己“做了个播放器”。但真实场景下用户点开 App 后切到微信回消息音乐必须继续响滑动进度条要精准跳转锁屏后通知栏得有暂停/上一首按钮甚至从文件管理器点击.mp3文件App 得能响应并播放——这些都不是MediaPlayer.start()一行代码能解决的。本篇聚焦「用原生 Android SDK 在 Android Studio 中构建一个生产级音乐播放器」的核心路径不依赖第三方 UI 框架如 ExoPlayer 的 UI 组件不引入 Kotlin DSL 或 Compose适配老项目更稳所有代码基于 Java XMLGradle 配置兼容 AGP 8.18.4适配 Android 10API 29至 Android 14API 34的存储访问模型Scoped Storage。适合刚完成《Android 开发入门》课程、正接手公司内部工具类 App 的中级开发者也足够让五年经验的工程师快速复用音频生命周期管理模块。2. 用 MediaPlayer Service 实现跨 Activity 播放控制为什么不用 ExoPlayer何时该换2.1 选型依据轻量、可控、无额外依赖的底层音频控制链ExoPlayer 功能强大但对仅需播放本地.mp3/.wav/.flac的工具类 App 来说它引入的exoplayer-core约 1.2MB、exoplayer-ui另加 400KB会显著拉长冷启动时间且其事件回调如onPositionDiscontinuity在后台被系统杀进程后行为不可控。而MediaPlayer是 Android Framework 层原生组件内存占用低于 150KBstart()/pause()/seekTo()命令毫秒级响应且与AudioManager、MediaSession天然耦合——这正是实现锁屏控制、蓝牙耳机按键响应、系统音量同步的基础。关键结论当项目目标是「稳定播放本地文件、支持基础控制、最小包体积」时MediaPlayer 是更优解。我们后续所有逻辑都围绕它展开不封装成“播放器 SDK”而是暴露MusicService的Binder接口供 Activity 调用。2.2 构建可通信的前台 Service避免 Android 8.0 后台服务限制从 Android 8.0Oreo起系统禁止应用在后台启动Service。若仅用startService()播放音乐App 切后台 10 秒内即被系统终止。正确做法是启动Service时调用startForeground()并绑定 Notification。以下为MusicService.java核心骨架public class MusicService extends Service { private MediaPlayer mediaPlayer; private final IBinder binder new LocalBinder(); private NotificationManager notificationManager; Override public void onCreate() { super.onCreate(); mediaPlayer new MediaPlayer(); // 关键设置音频流类型为 MUSIC否则系统音量调节无效 mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // 监听播放完成事件自动播放下一首后续扩展点 mediaPlayer.setOnCompletionListener(mp - { // TODO: 播放队列逻辑 }); notificationManager (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); } Override public int onStartCommand(Intent intent, int flags, int startId) { // 必须调用 startForeground否则 Android 8.0 无法保活 startForeground(1, buildNotification()); return START_STICKY; // 进程被杀后尝试重启 } private Notification buildNotification() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( music_playback, 音乐播放, NotificationManager.IMPORTANCE_LOW); notificationManager.createNotificationChannel(channel); } return new NotificationCompat.Builder(this, music_playback) .setContentTitle(正在播放) .setContentText(歌曲名) .setSmallIcon(R.drawable.ic_music) .setOngoing(true) // 禁止用户滑动清除 .build(); } Override public IBinder onBind(Intent intent) { return binder; } public class LocalBinder extends Binder { public MusicService getService() { return MusicService.this; } } // 暴露给 Activity 调用的控制方法 public void play(String filePath) { try { mediaPlayer.reset(); mediaPlayer.setDataSource(filePath); mediaPlayer.prepare(); // 同步准备适合小文件大文件用 prepareAsync() mediaPlayer.start(); } catch (Exception e) { e.printStackTrace(); } } public void pause() { if (mediaPlayer.isPlaying()) mediaPlayer.pause(); } public void seekTo(int msec) { mediaPlayer.seekTo(msec); } public int getCurrentPosition() { return mediaPlayer.getCurrentPosition(); } public int getDuration() { return mediaPlayer.getDuration(); } }提示startForeground()的id参数必须非零且Notification的setOngoing(true)是强制要求——这是系统识别“前台服务”的关键标识。若漏掉Service 会在后台运行几秒后被强制停止。2.3 在 AndroidManifest.xml 中声明 Service 并配置前台权限MusicService不是普通组件需显式声明并赋予前台服务权限service android:name.MusicService android:enabledtrue android:exportedfalse / !-- Android 9 需要此权限 -- uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / !-- 若需读取外部存储音乐文件Android 10 以下 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / !-- Android 11 使用 MediaStore API无需 READ_EXTERNAL_STORAGE -- uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO /注意android:exportedfalse表示该 Service 只能被本 App 内部组件绑定防止恶意 App 调用bindService()控制播放。READ_MEDIA_AUDIO是 Android 11API 30引入的细粒度权限替代了旧版READ_EXTERNAL_STORAGE申请时需动态弹窗。3. 在 MainActivity 中绑定 Service 并实现播放控制 UI从文件选择到进度条联动3.1 使用 MediaStore 查询本地音频文件绕过 Scoped Storage 限制的合规方案Android 10 强制启用 Scoped Storage直接new File(/sdcard/Music/)会因权限拒绝失败。正确做法是通过ContentResolver查询MediaStore.Audio.Media.EXTERNAL_CONTENT_URIprivate void loadAudioFiles() { String[] projection { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.DATA // 文件绝对路径 }; String selection MediaStore.Audio.Media.IS_MUSIC ! 0; Cursor cursor getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null, MediaStore.Audio.Media.TITLE ASC ); ListAudioItem audioList new ArrayList(); if (cursor ! null cursor.moveToFirst()) { do { String title cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); String artist cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); String path cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)); int duration cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)); audioList.add(new AudioItem(title, artist, path, duration)); } while (cursor.moveToNext()); cursor.close(); } // 更新 RecyclerView Adapter adapter.submitList(audioList); }逻辑说明MediaStore是系统维护的媒体数据库所有扫描到的音频文件自动入库。IS_MUSIC ! 0过滤掉铃声、通知音等非音乐文件DATA字段返回的是可直接传给MediaPlayer.setDataSource()的合法路径如/storage/emulated/0/Music/song.mp3无需手动拼接。3.2 实现可拖拽的 ProgressBar同步 MediaPlayer 当前位置与 UIXML 布局中定义ProgressBar并设为水平模式ProgressBar android:idid/progressBar style?android:attr/progressBarStyleHorizontal android:layout_width0dp android:layout_heightwrap_content android:max1000 !-- 占位最大值实际由代码设置 -- app:layout_constraintTop_toBottomOfid/tv_duration app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent /在 Activity 中启动定时更新每 500ms 刷新一次private Handler handler new Handler(Looper.getMainLooper()); private Runnable updateProgressRunnable new Runnable() { Override public void run() { if (musicService ! null musicService.isPlaying()) { int currentPosition musicService.getCurrentPosition(); int duration musicService.getDuration(); if (duration 0) { progressBar.setMax(duration); // 动态设最大值 progressBar.setProgress(currentPosition); tvCurrentTime.setText(formatTime(currentPosition)); } } handler.postDelayed(this, 500); } }; // 绑定 Service 后启动更新 private ServiceConnection serviceConnection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder service) { MusicService.LocalBinder binder (MusicService.LocalBinder) service; musicService binder.getService(); handler.post(updateProgressRunnable); // 开始刷新进度 } Override public void onServiceDisconnected(ComponentName name) { musicService null; handler.removeCallbacks(updateProgressRunnable); } };参数说明progressBar.setMax(duration)必须在duration 0时调用否则setProgress()会抛IllegalArgumentExceptionformatTime()将毫秒转为mm:ss格式是标准工具方法此处省略实现。3.3 处理进度条拖拽事件精准 seekTo 并防抖用户拖动ProgressBar时需将progress映射为MediaPlayer的毫秒值并避免高频触发progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { private boolean isUserDragging false; Override public void onStartTrackingTouch(SeekBar seekBar) { isUserDragging true; } Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser isUserDragging musicService ! null) { // 防抖只在松手后执行 seek避免拖动中频繁 seek 导致卡顿 // 实际项目中可加 Handler 延迟 200ms 判断是否最终位置 } } Override public void onStopTrackingTouch(SeekBar seekBar) { if (musicService ! null) { musicService.seekTo(seekBar.getProgress()); // seekTo 接收毫秒值 } isUserDragging false; } });关键细节onProgressChanged中fromUser为true才表示是用户操作而非代码setProgress()触发onStopTrackingTouch是唯一可靠的“拖拽结束”时机此时调用seekTo()最稳妥。4. 支持锁屏控制与系统媒体按钮用 MediaSession 实现跨应用交互4.1 初始化 MediaSession 并关联 MediaPlayer让系统知道“谁在播放”MediaSession是 Android 提供的标准化媒体控制接口系统音量键、蓝牙耳机物理键、锁屏界面均通过它通信。在MusicService.onCreate()中初始化private MediaSession mediaSession; Override public void onCreate() { super.onCreate(); // ... 其他初始化代码 mediaSession new MediaSession(this, MusicService); mediaSession.setActive(true); // 必须激活才能接收控制命令 // 将 MediaPlayer 关联到 MediaSession mediaSession.setPlaybackState(new PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_PAUSED, 0, 1.0f) .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) .build()); mediaSession.setCallback(new MediaSessionCallback()); }4.2 实现 MediaSession.Callback响应系统级控制指令MediaSessionCallback处理来自系统的所有控制请求private class MediaSessionCallback extends MediaSession.Callback { Override public void onPlay() { if (!mediaPlayer.isPlaying()) { mediaPlayer.start(); updatePlaybackState(PlaybackStateCompat.STATE_PLAYING); } } Override public void onPause() { if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); updatePlaybackState(PlaybackStateCompat.STATE_PAUSED); } } Override public void onSkipToNext() { // TODO: 播放下一首 updatePlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_NEXT); } Override public void onSkipToPrevious() { // TODO: 播放上一首 updatePlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS); } Override public void onSeekTo(long pos) { mediaPlayer.seekTo((int) pos); } private void updatePlaybackState(int state) { long position mediaPlayer.getCurrentPosition(); long duration mediaPlayer.getDuration(); mediaSession.setPlaybackState(new PlaybackStateCompat.Builder() .setState(state, position, 1.0f) .setActions(getAvailableActions(state)) .build()); // 同时更新 Notification 的播放状态图标 Notification notification buildNotification(); startForeground(1, notification); } private long getAvailableActions(int state) { long actions PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO; if (state PlaybackStateCompat.STATE_PLAYING) { actions | PlaybackStateCompat.ACTION_PAUSE; } else { actions | PlaybackStateCompat.ACTION_PLAY; } return actions; } }逻辑说明onPlay()/onPause()等回调由系统在用户按音量键、锁屏界面点击按钮时触发updatePlaybackState()不仅更新MediaSession状态还重建Notification以同步 UI如播放/暂停图标切换。4.3 在 Notification 中添加媒体控制按钮使用 RemoteViewsbuildNotification()方法需增强加入RemoteViews定义控制按钮private Notification buildNotification() { RemoteViews remoteViews new RemoteViews(getPackageName(), R.layout.notification_media); remoteViews.setTextViewText(R.id.tv_title, 当前歌曲); remoteViews.setTextViewText(R.id.tv_artist, 歌手名); // 播放/暂停按钮 Intent playIntent new Intent(this, MusicService.class); playIntent.setAction(PLAY); PendingIntent playPendingIntent PendingIntent.getService(this, 0, playIntent, PendingIntent.FLAG_IMMUTABLE); remoteViews.setOnClickPendingIntent(R.id.btn_play_pause, playPendingIntent); // 上一首 Intent prevIntent new Intent(this, MusicService.class); prevIntent.setAction(PREV); PendingIntent prevPendingIntent PendingIntent.getService(this, 1, prevIntent, PendingIntent.FLAG_IMMUTABLE); remoteViews.setOnClickPendingIntent(R.id.btn_prev, prevPendingIntent); // 下一首 Intent nextIntent new Intent(this, MusicService.class); nextIntent.setAction(NEXT); PendingIntent nextPendingIntent PendingIntent.getService(this, 2, nextIntent, PendingIntent.FLAG_IMMUTABLE); remoteViews.setOnClickPendingIntent(R.id.btn_next, nextPendingIntent); return new NotificationCompat.Builder(this, music_playback) .setContent(remoteViews) .setSmallIcon(R.drawable.ic_music) .setOngoing(true) .build(); }对应res/layout/notification_media.xml包含三个ImageButtonID 分别为btn_prev、btn_play_pause、btn_next。注意PendingIntent.FLAG_IMMUTABLE是 Android 12 强制要求否则getActivity()/getService()抛异常。5. 解决 Android Studio 开发中高频踩坑点从 Gradle 配置到真机调试5.1 Gradle 配置避坑AGP 8.1 下 MediaPlayer 兼容性关键参数Android Studio 2022.1.1AGP 8.1起默认启用android.useAndroidXtrue和android.enableJetifiertrue但MediaPlayer本身不依赖 AndroidX真正影响的是NotificationCompat。需确保app/build.gradle中android { compileSdk 34 defaultConfig { applicationId com.example.musicplayer minSdk 21 // MediaPlayer 在 API 21 完全稳定 targetSdk 34 versionCode 1 versionName 1.0 } // 关键禁用 Jetifier 对 MediaPlayer 无关类的干扰虽非必须但可减少日志噪音 dependencies { implementation androidx.core:core:1.12.0 // 必须NotificationCompat 所需 implementation androidx.media:media:1.6.0 // MediaSessionCompat 所需 } }参数说明minSdk 21是底线——MediaPlayer在 API 21Lollipop才支持setDataSource(FileDescriptor)等关键方法targetSdk 34确保适配 Android 14 的后台限制androidx.media:media是MediaSessionCompat的现代实现比旧版android.support.v4.media更可靠。5.2 真机调试常见问题排查表现象根本原因解决方案MediaPlayer.prepare()报IOException文件路径非法或权限不足检查MediaStore查询返回的DATA字段是否为空Android 11 确认已申请READ_MEDIA_AUDIONotification 点击无响应PendingIntent的FLAG_IMMUTABLE缺失将PendingIntent.getService(..., FLAG_IMMUTABLE)替换所有FLAG_ONE_SHOT进度条不更新Handler在onServiceDisconnected后未移除handler.removeCallbacks(updateProgressRunnable)必须在onServiceDisconnected中执行锁屏后播放停止startForeground()未调用或Notification未setOngoing(true)检查onStartCommand()是否执行startForeground()且Notification构造时调用setOngoing(true)蓝牙耳机按键无反应MediaSession未setActive(true)在MediaSession初始化后立即调用setActive(true)不能延迟5.3 在 Android Studio 中快速验证 MediaPlayer 状态ADB 命令行诊断法不依赖 UI直接通过 ADB 查看MediaPlayer底层状态# 查看当前活跃的 MediaSession确认你的 Service 已注册 adb shell dumpsys media_session # 查看 MediaPlayer 状态需 root但开发机可试 adb shell su -c dumpsys media.player # 强制停止 Service用于清理测试状态 adb shell am force-stop com.example.musicplayer技巧dumpsys media_session输出中搜索com.example.musicplayer/MusicService若看到activetrue且statePLAYING说明MediaSession已生效若stateNONE则setActive(true)未执行或被异常中断。本文还有配套的精品资源点击获取