ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

98分安卓音乐播放器课程设计:MediaPlayer+Service后台播放实战解析

98分安卓音乐播放器课程设计:MediaPlayer+Service后台播放实战解析 简介这是一份面向计算机相关专业学生的安卓开发课程设计资源定位为可直接参考的高分项目源自大三学期经导师指导并获得98分评审的期末大作业适用于课程设计、期末大作业或项目实战练习可帮助学习者理解Android音乐播放器的完整实现思路。压缩包共96个文件大小29.09MB以Java源码、XML界面布局、WebP/JPG/PNG图片素材为主同时包含Gradle构建脚本、ProGuard混淆配置和MP3试听音频目录结构完整便于移植和二次开发。目前已有191人学习下载。资源提供完整Android音乐播放器项目源码涵盖主工程app目录、构建配置与界面展示图片可直接查看核心代码与效果图适合需要快速搭建音乐播放器功能或梳理安卓开发项目框架的学习者作为课程设计参考具有较高实用价值。1. 一个 98 分的音乐播放器课程设计值得拆开看什么如果这学期你也在为安卓开发课程设计发愁大概率遇到过这样的场景翻遍 GitHub 找不到一个「既不太大又能讲清楚」的音乐播放器项目拿网上的快餐 Demo 去答辩又经不起导师追问。这个评定为 98 分的 MusicPlayer 项目价值不在界面有多炫而在于它把安卓开发里最有含金量的几条主线都串起来了MediaPlayer 状态机管理、Service 后台播放、Binder 跨组件通信、通知栏前台服务以及 Android 6 到 13 的存储权限适配。对正在做期末大作业或想找个完整实战项目练手的人来说源码读一遍、抄一遍、改一遍基本就能应付答辩里“你这个播放器怎么做到后台不中断”之类的核心问题。下面按我拆项目时的顺序把这套代码从架构到排错完整过一遍。2. 项目结构与播放架构从 Gradle 配置看项目能跑在哪拿到MusicPlayer-master.zip解压后先别急着点app/src/main/java。我习惯先看构建配置和整体目录这决定了你能不能在一台新电脑上三分钟内跑起来。2.1 目录结构对应的职责边界项目根目录下的关键文件分为三类。settings.gradle、根build.gradle、gradle.properties、gradle/wrapper/gradle-wrapper.properties属于构建链路app/build.gradle、app/proguard-rules.pro属于模块级配置app/src/main/java/MusicPlayer-master/下的包结构才是业务代码。.idea和vcs.xml说明这是用 Android Studio 打开后直接配好 Git 的工程不需要额外导入步骤。业务代码一般是按三层组织的界面层放 Activity 和 Adapter负责 RecyclerView 列表渲染和播放控制按钮的点击回调播放核心层放一个常驻的 Service内部持有 MediaPlayer 实例数据层用 ContentResolver 查询 MediaStore。这里最关键的判断点是如果AndroidManifest.xml里MainActivity配置了launchModesingleTask说明项目处理了播放页和列表页互相跳转时的实例复用问题——播放状态不会因为 Activity 重建而丢失。2.2 app/build.gradle 里的三个关键配置app/build.gradle决定了这个项目能在哪个编译级别上运行也决定了你导入时会不会报错。一个可跑的教学项目核心配置通常长这样android { compileSdkVersion 33 defaultConfig { applicationId com.example.musicplayer minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName 1.0 } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } } dependencies { implementation androidx.appcompat:appcompat:1.6.0 implementation androidx.recyclerview:recyclerview:1.3.0 implementation com.google.android.material:material:1.9.0 }这里有两个容易踩坑的点。第一minSdkVersion 21意味着项目用不到API 21以下的老古董接口凡是网上让你用getExternalStorageDirectory()读 SD 卡的写法在这个项目里都已经被MediaStore查询替代了。第二targetSdkVersion 33对应 Android 13这个版本开始读取本地音频需要申请READ_MEDIA_AUDIO权限而minSdkVersion 21的设备仍然走READ_EXTERNAL_STORAGE所以代码里必须做版本判断——这个我们留到第 4 章排错时细说。2.3 AndroidManifest 里的播放器“骨架声明”AndroidManifest.xml是判断一个音乐播放器项目完整性的最快途径。除了MainActivity合格的课程设计还会有这几个声明播放 Service、前台服务权限、录音/存储权限。一个典型的声明块是这样uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.WAKE_LOCK / application service android:name.service.PlayService android:enabledtrue android:exportedfalse / /application注意READ_EXTERNAL_STORAGE上面挂的maxSdkVersion32这是在 Android 13 设备上避免重复弹窗的标准做法。exportedfalse表示该 Service 只允许应用内部组件通过显式 Intent 绑定外部应用无法唤起这是上架审查的基本要求。WAKE_LOCK权限的作用容易被低估——它能让手机息屏后 CPU 继续运行保证后台播放不卡顿。在课程设计答辩时如果你能把“为什么要 WAKE_LOCK、不加的话息屏后音频会不会断续”讲清楚是很加分的点。3. MediaPlayer Service 播放核心状态机与生命周期这一章是整个项目最该读透的部分。很多课程设计翻车全是因为MediaPlayer用成了“创建对象 → start() → 完事”完全没考虑状态流转和资源释放。结果是切歌崩一次、退出崩一次、来电再崩一次。3.1 MediaPlayer 状态流转不是所有方法随时都能调MediaPlayer本质是一个状态机。new出来是Idle态setDataSource()之后进入Initialized态prepare()/prepareAsync()完成后进入Prepared态此时才能调用start()。进入Started态后可以pause()到Paused态也可以stop()到Stopped态。Stopped态要重新prepare()才能回到Prepared。任何一个状态错位调用接口直接抛IllegalStateException。项目里播放控制部分的代码通常长这样private void playTrack(String path) { if (mediaPlayer null) { mediaPlayer new MediaPlayer(); } try { mediaPlayer.reset(); // 无论是首次创建还是切换歌曲先回到 Idle 态 mediaPlayer.setDataSource(path); mediaPlayer.setOnPreparedListener(mp - { mp.start(); updateNotification(); // 启动前台服务并刷新通知栏 }); mediaPlayer.prepareAsync(); // 网络/大文件用异步准备避免阻塞 UI 线程 } catch (IOException e) { Log.e(PlayService, setDataSource failed: path, e); } }这段代码的关键在reset()。MediaPlayer对象在onDestroy里没有完全释放时切换歌曲直接调setDataSource()会残留上一首的资源所以先reset()回到Idle态最稳妥。prepareAsync()是异步准备播放本地大文件时不会 ANR但它的回调onPrepared是在主线程执行的里面不要做耗时操作。3.2 Service 双重启动模式START_STICKY 与绑定通信音乐播放器的主服务建议用startService()和bindService()同时调用。startService()保证服务即使没有 Activity 关联也能运行切换后台不会停bindService()用来拿到Binder对象让 Activity 能直接调用播放、暂停、切歌这些方法。核心代码骨架如下public class PlayService extends Service { private final IBinder binder new LocalBinder(); public class LocalBinder extends Binder { public PlayService getService() { return PlayService.this; } } Override public int onStartCommand(Intent intent, int flags, int startId) { // 返回 START_STICKY进程被系统杀掉后尝试重建服务并传入 null Intent return START_STICKY; } Nullable Override public IBinder onBind(Intent intent) { return binder; } }onStartCommand返回START_STICKY的含义是如果系统在内存不足时把播放服务所在的进程杀掉了等内存恢复后系统会尝试重建这个 Service。对音乐播放器来说START_STICKY是最合适的策略——它有概率导致重建时intent为 null所以业务逻辑不要把启动参数只从intent里拿要设计成“重建后自动恢复播放列表和播放进度”。Activity 侧绑定并持有 Service 引用private PlayService playService; private ServiceConnection connection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder binder) { playService ((PlayService.LocalBinder) binder).getService(); playService.restoreState(); // 恢复播放进度和列表索引 } Override public void onServiceDisconnected(ComponentName name) { playService null; } };3.3 前台服务与通知栏为什么播放时必须有可见通知从 Android 8.0API 26开始后台应用不能随意启动 Service但有一个例外——前台服务也就是带通知栏常驻通知的服务。音乐播放器在后台播放时必须调用startForeground(id, notification)把自己标记为前台服务不然运行几秒就会被RemoteServiceException杀掉。通知栏控制的实现常见做法是创建一个NotificationCompat.MediaStyle通知塞三个动作按钮播放/暂停、上一首、下一首再挂一个PendingIntent关联到 MainActivity。这样用户下拉通知栏直接能控制播放。代码示例NotificationCompat.Builder builder new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_music_note) .setContentTitle(currentSong.getTitle()) .setContentText(currentSong.getArtist()) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentIntent(mainPendingIntent) .addAction(R.drawable.ic_previous, 上一首, prevPendingIntent) .addAction(R.drawable.ic_play, playPauseText, playPausePendingIntent) .addAction(R.drawable.ic_next, 下一首, nextPendingIntent) .setStyle(new NotificationCompat.MediaStyle() .setShowActionsInCompactView(1)); // 折叠态只显示播放/暂停按钮 startForeground(1, builder.build());setShowActionsInCompactView(1)是细节高亮通知栏折叠时只露出下标为 1 的播放/暂停按钮这样视觉效果干净。有些项目会把三个按钮全塞进setShowActionsInCompactView折叠态会比较拥挤写论文时也讲不出这个设计的依据。3.4 本地音乐扫描MediaStore 查询与分区存储适配安卓 10 以后强制分区存储getExternalStorageDirectory()基本只能访问应用专属目录读公共音乐库必须走 MediaStore。课程设计里扫描手机歌曲的代码标准写法是public ListSong queryLocalSongs() { ListSong songs new ArrayList(); Uri collection MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String[] projection { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.DATA // DATA 字段在 API 29 可能访问受限但用于 MediaPlayer.setDataSource 仍可用 }; String selection MediaStore.Audio.Media.IS_MUSIC ! 0; String sortOrder MediaStore.Audio.Media.TITLE ASC; try (Cursor cursor getContentResolver().query(collection, projection, selection, null, sortOrder)) { if (cursor ! null) { while (cursor.moveToNext()) { Song song new Song(); song.setId(cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))); song.setTitle(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE))); song.setArtist(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST))); song.setDuration(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION))); song.setPath(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA))); songs.add(song); } } } return songs; }这里选IS_MUSIC ! 0能过滤掉通知音、铃声这类短音频避免列表里混进乱七八糟的音频文件。Cursor用try-with-resources包裹保证查询完成后自动释放——这个细节在课程设计报告中可以作为“内存优化点”写进去。4. 构建、运行与机型适配排错从导入到跑通的完整路径这一章是把代码变成一个能在自己手机上跑的 APK 的关键。多数人卡住不是在代码逻辑上而是 Android Studio 的环境差异和厂商 ROM 的权限策略上。按下面顺序走能少踩一半的坑。4.1 从导入到真机安装的标准操作在 Android Studio 里直接用Open an existing project选中MusicPlayer-master目录别选外层的 zip 解压目录套一层。等待 Gradle Sync 完成后先做三件事第一菜单栏Build - Clean Project第二检查File - Project Structure - SDK Location里 Android SDK 路径是否存在且已安装对应版本的Build-Tools第三看gradle-wrapper.properties里distributionUrl指向的 Gradle 版本AGPAndroid Gradle Plugin8.0 左右对应的 Gradle 版本一般不低于 8.0本地没有会自动下载但需要网络通畅。场景推荐设备条件验证目标首次运行模拟器 API 30权限弹窗、列表加载是否正常课程演示真机 API 33通知栏控制、后台播放是否稳定兼容性检查低版本备用机 API 23老机型存储权限路径分支是否生效我一般建议优先用真机跑播放器项目。模拟器的音频过路由延迟MediaPlayer 的onCompletion回调时机在模拟器上不准确不利于调试切歌逻辑。4.2 高频报错逐个拆解错误一D8: tag number over 30 is not supported。这个报错通常出现在重复依赖了不同版本的 support-library 或 AndroidX 时。原因是 dex 文件里的方法引用标签溢出常见于项目里同时引用了老版com.android.support包和 AndroidX 包。排查路径是执行终端命令./gradlew :app:dependencies --configuration debugCompileClasspath deps.txt然后搜索support关键字找到混用依赖统一迁移到 AndroidX 即可。在 Android Studio 里可以用Refactor - Migrate to AndroidX一键转换但注意要提前备份因为该操作会重写包名引用。错误二权限弹窗点“允许”后列表仍为空。这个大概率是requestPermissions回调里拿到授权以后没有重新查询 MediaStore。正确的流程是Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { loadSongs(); // 重新执行 ContentResolver 查询 adapter.notifyDataSetChanged(); // 刷新 RecyclerView } }注意在 Android 13 上除了READ_MEDIA_AUDIO在 Android 14 及以上你还需要处理READ_MEDIA_VISUAL_USER_SELECTED需要部分照片/视频授权场景音乐播放器项目可以不用管这个但答辩时能提一句说明你关注过 targetSdk 34 的变化。错误三java.lang.IllegalStateException: Unable to create service com.example...PlayService。多半是 Manifest 里 Service 没有正确声明或者声明了但android:name没带点号前缀。检查AndroidManifest.xml中的 service 标签是否写在application内且类名路径正确。4.3 adb 验证服务与播放状态手机上装好 APK 后用 adb 可以验证 Service 是否还活着。播放一首歌后锁屏过几十秒执行adb shell dumpsys activity services | grep -E PlayService|isForeground如果输出里有isForegroundtrue说明前台服务正常。如果找不到服务大概率是被系统杀掉了——优先检查onStartCommand返回值和通知渠道是否创建。再配合adb logcat抓 MediaPlayer 相关日志adb logcat -s MediaPlayer:V PlayService:V切歌时如果看到MediaPlayerNative: error (1, -2147483648)一般是文件路径无效或编码格式不支持。Android 原生 MediaPlayer 对格式支持有限FLAC、APE 这类无损格式在部分机型上播不了遇到这种情况先换成同一首歌的 MP3 文件验证排除。5. 把课程设计改成能答辩的项目播放模式、ExoPlayer 替换与验证清单如果只是让项目跑通导师大概率只给个及格分。把下面几个点改进去代码量和答辩深度会明显不一样。5.1 播放模式切换与 onCompletion 自动连播顺序播放、单曲循环、随机播放这三种模式核心区别只在onCompletion回调里的索引计算策略。项目里通常在设置里存一个play_mode的 SharedPreferences然后在MediaPlayer.OnCompletionListener中根据模式计算下一首索引switch (playMode) { case MODE_LOOP_ALL: nextIndex (currentIndex 1) % playlist.size(); break; case MODE_LOOP_ONE: nextIndex currentIndex; // 重新播放同一首 break; case MODE_RANDOM: nextIndex new Random().nextInt(playlist.size()); break; }这里容易忽略的是MODE_LOOP_ONE不需要重新setDataSource直接调mediaPlayer.seekTo(0)然后start()即可——这能减少一次prepareAsync的耗时切换几乎无感知面试聊到会有印象分。5.2 替换 ExoPlayer 的接口隔离思路原生 MediaPlayer 在小项目里够用但要说“可扩展性”答辩被追问时最好的答案是“我已经做好了替换 ExoPlayer 的接口隔离”。做法是定义一个PlaybackController接口把play()、pause()、seekTo()、release()全部抽象出去public interface PlaybackController { void play(String path); void pause(); void seekTo(int position); void release(); }MediaPlayer 实现类封装原来的调用ExoPlayerImpl则用ExoPlayer加MediaItem实现。真正想切的时候只改 Service 里的实现类实例化这一行UI 层完全不动。这个解耦思路比换个播放器本身更能体现设计能力。5.3 退出清理与进度记忆验证播放器最容易被忽略的场景是“退出应用再进来”。Service 里要维护一个currentPosition字段在onPause或onDestroy前保存到 SharedPreferences在onCreate或onStartCommand里读回来配合seekTo(position)恢复。验证方法很简单播放到 1 分 30 秒杀掉应用重新打开点上次播放的歌曲看是否从 1 分 30 秒附近开始。释放资源时按依赖倒序清理Override public void onDestroy() { if (mediaPlayer ! null) { if (mediaPlayer.isPlaying()) mediaPlayer.stop(); mediaPlayer.release(); // 释放 native 资源 mediaPlayer null; } super.onDestroy(); }5.4 答辩前绕不开的自检清单真机装好项目后按这条路径过一遍普通播放 → 切后台 → 锁屏 → 等 1 分钟 → 亮屏下拉通知栏 → 点“播放/暂停”三次 → 切三首歌 → 插拔耳机一次 → 关闭应用。任何一个环节出现闪退、无声、通知栏无响应都回到第 3 章的对应代码块里找问题。如果测试到“插拔耳机”时声音还在外放这其实是加分题——在项目里监听ACTION_AUDIO_BECOMING_NOISY广播并暂停播放写进报告里就是一条完整的体验优化记录。把这一行广播注册代码加进PlayService课程设计的“功能完整性”评分基本就稳了。本文还有配套的精品资源点击获取
返回列表