简介这是基于Android Studio开发的安卓音乐播放器APP完整项目源码面向计算机专业正在准备课程设计或期末大作业的学生也适合需要项目实战练习的入门学习者。项目源自大三期末大作业经导师指导获99分高分代码完整、可直接运行小白也能按步骤完成部署与演示。压缩包共375个文件约4.13MB包含116个Java源文件、74个XML布局与配置、161张PNG图片资源以及Gradle构建脚本、APK安装包等覆盖界面绘制、逻辑实现到打包安装的完整链路。已有273人学习下载。通过该项目可掌握Activity与Service协作、MediaPlayer播放控制、通知栏管理、自定义列表适配器等Android核心知识点同时可作为答辩讲解和功能演示的可靠素材目录结构规范便于快速定位代码与资源且附带APK可直接安装体验是期末提分和项目实战的高质量参考。1. 为什么「音乐播放器」是 Android Studio 期末大作业里最值得做的题目每年期末季Android Studio 课程设计题目翻来覆去就那么几个记账本、天气助手、待办事项、音乐播放器。前三个都是「页面 数据库」的堆叠唯独音乐播放器要把多媒体框架、后台任务、系统服务、通知栏、生命周期全部串在一起知识密度完全不在一个量级。换句话说同样的工作量音乐播放器在答辩时能讲的内容比记账本多一倍。很多人觉得播放器难是因为一开始就奔着「要做一个网易云」去结果卡在歌词滚动、网络请求、VIP 解密这些地方。期末作业的正确姿势是把范围收窄本地音频扫描 MediaPlayer 播放 前台服务保活 通知栏控制 播放列表这套组合已经足够拿高分而且每一块都有明确的验收点。本文不教你写一个完整 App而是把「从空项目到能演示的播放器」这条路上最容易卡住的节点逐个拆开每个节点给出可直接抄的代码、参数和排错思路。2. MediaPlayer 状态机音乐播放器 App 的地基2.1 为什么 MediaPlayer 比 SoundPool 和 ExoPlayer 更适合期末作业做安卓音乐播放器音频播放方案有三条路MediaPlayer、SoundPool、ExoPlayer。SoundPool 面向短音效延迟低但解码长音频内存会出问题ExoPlayer 是 Google 官方推荐的高阶方案支持自适应码率和流媒体但对期末作业来说接入成本过高——你需要引入两个依赖、理解Player.Listener回调、还要处理MediaItem的构建答辩时容易把自己绕进去。MediaPlayer 是 Android 原生多媒体框架中最经典的一层封装它内部基于 Stagefright 引擎支持常见音频格式解码API 设计直接对应「播放 / 暂停 / 停止 / 跳转」这些基础操作。Google 在官方文档中明确写了它的状态转换图也就是说考试里常见的那张图不是故作高深而是MediaPlayer底层真实的工作流程。用MediaPlayer.create()创建实例后它会直接进入 Prepared 状态不用手动调prepare()这对新手极其友好。2.2 从零开始的最小编播放骨架初始化播放器的代码看起来简单但放在 Activity 里和放在 Service 里完全是两种写法。先看最基础的版本在 Activity 中播放本地 res 目录下的音频// 注意这是传入的资源 ID不是文件路径 MediaPlayer mediaPlayer MediaPlayer.create(this, R.raw.sample_music);create()是静态方法内部完成了 setDataSource prepare 两步。这里最容易被忽略的是资源文件位置R.raw目录默认不存在需要手动在main/res下新建raw文件夹然后把 mp3 或 wav 文件拖进去文件名必须是小写字母和下划线组合否则直接编译失败。两行代码就能出声但期末作业要的显然不止出声。真正会踩坑的是从资源播放切换到播放手机本地存储的音频文件数据源变了状态机的行为也就变了MediaPlayer mediaPlayer new MediaPlayer(); try { // 传的是文件的绝对路径 mediaPlayer.setDataSource(/storage/emulated/0/Music/example.mp3); mediaPlayer.prepare(); // 耗时操作必须放在子线程 } catch (IOException e) { e.printStackTrace(); }代码逻辑说明与create()不同使用setDataSource()后播放器处于 Initialized 状态必须显式调用prepare()或prepareAsync()才能进入 Prepared 状态。这里的prepare()是阻塞调用如果音频文件在网络上或体积较大会卡住主线程触发 ANR因此实际项目中要放到子线程。参数说明setDataSource()接收的是 String 路径从 Android 10 开始直接访问/storage/emulated/0下的文件需要申请READ_EXTERNAL_STORAGE权限Android 13 及以上则改用READ_MEDIA_AUDIO权限具体申请方式在后面的章节细说。2.3 实现本地音乐列表扫描从文件路径到播放队列播放器的灵魂是播放列表而播放列表的第一步是扫描本地音频。遍历 SD 卡目录属于「人人都知道怎么写但很少有人写对」的环节用File递归访问很容易把Android/data目录下的非音乐文件也扫进来。通常的做法是用ContentResolver查询媒体数据库public ListAudioModel loadLocalAudio() { ListAudioModel result new ArrayList(); ContentResolver resolver getContentResolver(); 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 }; String selection MediaStore.Audio.Media.IS_MUSIC ! 0; Cursor cursor resolver.query(collection, projection, selection, null, null); if (cursor ! null) { while (cursor.moveToNext()) { AudioModel audio new AudioModel(); audio.setTitle(cursor.getString(1)); audio.setArtist(cursor.getString(2)); audio.setDuration(cursor.getInt(3)); audio.setPath(cursor.getString(4)); result.add(audio); } cursor.close(); } return result; }逻辑说明MediaStore.Audio.Media.EXTERNAL_CONTENT_URI是系统媒体库的对外接口App 只在第一次扫描或刷新数据时才会真正读取文件元数据平时查询走的是 SQLite 索引效率远超直接遍历目录。其中IS_MUSIC ! 0这个条件用于过滤掉录音、通知音等非音乐类文件。参数说明projection中DATA字段存的是文件的绝对路径通过cursor.getString(4)拿到的值可以直接传给MediaPlayer.setDataSource()。但个别国产 ROM 出于隐私保护原因对DATA字段的返回做了限制如果扫描结果为空可以改用MediaStore.Audio.Media.RELATIVE_PATH字段拼接获取路径。3. 前台服务 通知栏控制让音乐播放器后台也能跑3.1 为什么后台播放必须要 Service 和 PendingIntentActivity 一销毁MusicPlayer 的进度就停这是期末答辩时最容易被追问的问题。Android 的进程管理机制决定了当 Activity 不可见时系统随时可能回收它占用的资源。要保证音乐持续播放唯一的正解是启动一个前台服务让进程优先级提升到前台进程级别。前台服务不是普通的 Service。前台服务的核心是「必须在启动后 5 秒内调用startForeground()并传入一个通知栏 Notification否则系统会抛出ForegroundServiceDidNotStartInTimeException并直接杀掉进程」。也就是说通知栏上那个常驻的播放器卡片不只是给用户看的也是系统判定「这个进程不能杀」的依据。安卓 12 用了新的前台服务启动限制从后台启动前台服务会被拦截。实际操作中更常见的方案是App 在前台时启动服务播放过程通过bindService与 MediaPlayer 所在的 Service 建立双向通信这样不仅能把播放控制逻辑从 Activity 中剥离还能让音乐在用户切到其他 App 后继续播。3.2 极简前台服务骨架从 onStartCommand 到通知栏先看接口层面音乐播放器的 Service 至少要提供这些方法play()、pause()、playNext()、playPrevious()、seekTo()和getCurrentPosition()。用 Binder 的方式实现可以避免自己手写一堆 Intent 通信代码public class MusicService extends Service { private MediaPlayer mediaPlayer; private final IBinder binder new MusicBinder(); public class MusicBinder extends Binder { MusicService getService() { return MusicService.this; } } Override public void onCreate() { super.onCreate(); // 这里创建 MediaPlayer而不是在 Activity 里创建 mediaPlayer new MediaPlayer(); mediaPlayer.setAudioAttributes( new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ); } Override public void onStartCommand(Intent intent, int flags, int startId) { // 处理来自通知栏按钮的指令 String action intent.getAction(); if (ACTION_PLAY.equals(action)) { mediaPlayer.start(); } else if (ACTION_PAUSE.equals(action)) { mediaPlayer.pause(); } return START_NOT_STICKY; } }逻辑说明使用bindServicestartService双机制启动的原因在于两者职责不同——startService保证音乐播放过程中 Service 不被系统回收bindService则提供 Activity 与 Service 之间方法调用的通道。代码中的AudioAttributes是必配项它声明了这个播放器的音频类型是媒体音乐。参数说明onStartCommand的返回值START_NOT_STICKY表示如果 Service 因内存不足被杀系统不自动重启。这个选择对音乐播放器是合理的因为你无法确定被杀前用户正在听哪首歌如果用START_STICKY系统恢复 Service 后会给你一个空的 Intent反而容易处理出错。通知栏的实现同样有讲究前台服务要求通知在startForeground()时立即出现所以要预先构建通道。Android 8 以上必须有 NotificationChannel否则通知不会展示String channelId music_playback; NotificationChannel channel new NotificationChannel( channelId, 播放控制, NotificationManager.IMPORTANCE_LOW ); NotificationManager manager getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); // 构建通知 Notification notification new NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.ic_music) .setContentTitle(Now Playing) .setContentText(currentSong.getTitle()) .setShowWhen(false) .setForegroundServiceBehavior( NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .build(); startForeground(1001, notification);逻辑说明IMPORTANCE_LOW是这里的关键参数它表示通知不会弹横幅和发声只在状态栏上静默常驻。如果误用IMPORTANCE_HIGH每切一首歌都响一次提示音这显然是错误的做法。通知栏上的「上一首 / 播放暂停 / 下一首」按钮本质上是一组挂上PendingIntent的NotificationCompat.Action每个 Action 都要带一个唯一的 requestCode这样系统才能区分点的是哪个按钮Intent playIntent new Intent(this, MusicService.class); playIntent.setAction(ACTION_PAUSE); PendingIntent playPendingIntent PendingIntent.getService(this, 0, playIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);参数说明FLAG_IMMUTABLE是 Android 12 的强制要求targetSdk 31 及以上如果这里不写运行时会直接崩。FLAG_UPDATE_CURRENT表示如果之前有同一个 requestCode 的 PendingIntent就用新的 Intent 数据更新它。3.3 Activity 绑定 Service 的固定套路Activity 里绑定 Service 有固定的三步bindService、实现ServiceConnection、在onDestroy中解绑。真正的坑在解绑时机——如果 Service 还被绑定着unbindService()会触发IllegalArgumentException这个问题在屏幕旋转时尤其常见。应对方法是在 Activity 的onStart中绑定、在onStop中解绑并加一个布尔标志防止重复解绑。是不是看起来有点绕换个说法凡是bindService调用成功后就一定存在一个「需要解绑」的状态你要做的是保证unbindService只调用一次且必须在onDestroy之前。网上有大量「Activity 重启后 Service 连接断开还疯狂报错」的求助帖根源基本都是这个。4. 音频焦点与生命周期这是音乐播放器 android 开发的核心分水岭4.1 AudioFocus不处理就会被打低分的隐藏考点很多播放器大作业能播歌、能切歌但一开音乐 App 就自己出声把当前正在播的音乐顶掉。面试官或答辩老师只要问一句「你有没有处理 AudioFocus」八成的人会愣住。AudioFocus 是 Android 多媒体框架中的互斥机制它保证多个应用不会同时出声典型场景是你的播放器正在放歌导航 App 播报语音时会让你的音乐暂时压低音量播报结束后恢复。MediaPlayer 不会被系统自动处理音频焦点必须自己申请、自己监听AudioManager audioManager (AudioManager) getSystemService(AUDIO_SERVICE); AudioFocusRequest focusRequest new AudioFocusRequest.Builder( AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes( new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() ) .setOnAudioFocusChangeListener(focusChangeListener) .build(); int result audioManager.requestAudioFocus(focusRequest); if (result ! AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { return; // 没有拿到焦点不能开始播放 }逻辑说明AUDIOFOCUS_GAIN适用于你的 App 独占播放的场景比如用户主动点了一首歌。AUDIOFOCUS_GAIN_TRANSIENT适合临时需要音频的场景比如播放提示音这个类型被允许打断其他应用的播放。监听器回调里拿到的focusChange有四种取值常见的处理逻辑是LOSS时暂停并释放焦点LOSS_TRANSIENT时暂停但保留焦点GAIN时恢复播放。这套代码放进期末大作业里一眼就能看出你对安卓开发的系统级资源管理有理解——这比多做两个换肤界面值钱得多。4.2 播放状态与 Activity 生命周期联动音乐播放器的生命周期处理有个反直觉的结论Activity 的onPause不暂停音乐onDestroy才考虑停止服务。原因是音乐本身的播放状态由 Service 内的 MediaPlayer 维护与 Activity 是否可见无直接关联。但在用户离开页面时记住并保存进度是合理需求做法是监听onStop把进度写入SharedPreferencesOverride protected void onStop() { super.onStop(); if (mediaPlayer.isPlaying()) { int position mediaPlayer.getCurrentPosition(); // 保存进度下次进入直接恢复 getSharedPreferences(play_state, MODE_PRIVATE) .edit() .putInt(last_position, position) .putString(last_song_id, currentSong.getId()) .apply(); } }逻辑说明getCurrentPosition()的单位是毫秒保存时无需转换。这里用MODE_PRIVATE是因为播放进度属于本应用内部的私有数据不应该对其他应用可见。很多播放器在界面划走再回来时进度条归零、播放按钮状态重置而不是因为生命周期没处理好而是因为 Activity 被销毁重建后没有从 Service 侧拉取最新状态。解决的核心是「以 Service 为准」Activity 的onStart中主动查询 Service 的状态并刷新 UI。4.3 Android Studio 编译期三座大山SDK 勾选、AGP 版本、中文汉化期末周最多人挂在编译上而不是写在代码上。先列一个高频报错清单。Android Studio sdk 无法勾选这个经典问题通常发生在刚装完 Android Studio 时SDK Manager 里所有平台版本都显示灰色无法勾选。原因是安装时没有配置 SDK 目录的写权限Windows 上尤其容易触发。常见解法是用管理员身份运行 Android Studio或者在local.properties里配置sdk.dir指向已有 SDK 路径后重启。Build 出现 tag number over 30 is not supported是另一种典型这个报错不是你的代码问题而是构建工具版本和 JDK 版本不匹配。Android Studio 的新版本内置了 JBRJetBrains Runtime但如果你在gradle.properties里强行指定了较旧的 JDK 路径就会出现tag number异常。解决方法是把项目用的 SDK 平台和 Build Tools 版本对齐具体就是打开build.gradle将compileSdk、targetSdk和buildToolsVersion统一设置成同一个大版本。最后说android studio 怎么设置中文。新版 Android Studio 不需要额外汉化包直接在Settings - Plugins里搜索「Chinese」安装官方中文语言包重启即可。但注意汉化只影响 IDE 界面不影响代码和编译日志报错信息仍然是英文的——答辩演示时如果卡在编译报错不要慌看Build Output窗口里标红的error:行对照上面的逻辑排查。5. 实机调试与运行技巧让音乐播放器在手机上稳定跑通5.1 真机调试三步配置从开发者选项到无线连接模拟器虽然有音频输出但性能和音频硬件模拟都不如真机。期末演示如果用真机调试配置要先过关。打开设置 - 关于手机连续点击版本号 7 次激活开发者模式然后进入开发者选项开启USB 调试。用数据线连接电脑后Android Studio 会在Run面板自动识别设备。如果数据线连接不稳定Google 在 Android 11 推出了无线调试功能同一 Wi-Fi 下可以告别数据线先用 USB 连接手机并打开无线调试然后在Android Studio - Device Explorer中选用「Pair using QR code」或配对码连接。这个方案在 xiaomi 这类对 USB 调试限制较多的机型上意外好用。安卓项目连接手机后最频繁出现的错误是安装失败日志里写着INSTALL_FAILED_UPDATE_INCOMPATIBLE。原因很简单手机上已经装过同包名的应用但签名不一致导致覆盖安装被拒比如之前用了一台电脑的 debug 密钥换了电脑或换了签名后就会遇到。解法是在手机设置里卸载原有应用再重新安装。5.2 动态权限申请的正确位置不要写在 MainActivity 里读本地音乐离不开存储权限。Android 6 及以上要求动态申请权限Android 13 开始把音频读取权限细分为READ_MEDIA_AUDIO。放到 Android Studio 开发的环境下现在的写法是这样的private void requestAudioPermission() { if (Build.VERSION.SDK_INT 33) { // Android 13 使用细分的音频权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_MEDIA_AUDIO}, 1001); } } else { // Android 12 及以下 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1001); } }代码逻辑说明Android 13 引入了分区存储的加强策略阅读音频文件、图片、视频分别对应三个独立权限。如果你的targetSdk设置为 33 及以上在 Android 13 设备上只申请READ_EXTERNAL_STORAGE是无效的。参数说明1001是请求码需要与onRequestPermissionsResult回调中的requestCode对应。这里的常见误用是把请求码定义在各处分散的数字导致回调漏判——定义一个全局常量PERMISSION_REQUEST_CODE更稳妥。5.3 权限拒绝后的优雅降级与再次请求一个被忽略但答辩必问的细节是用户点击「拒绝」后列表扫描不到音乐App 直接白屏。正确的做法是在onRequestPermissionsResult中做二次判断如果用户拒绝引导去系统设置开启权限代码是这样的Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode PERMISSION_REQUEST_CODE) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { loadLocalAudio(); } else { // 区分「拒绝一次」和「不再询问」 if (!ActivityCompat.shouldShowRequestPermissionRationale( this, Manifest.permission.READ_MEDIA_AUDIO)) { // 用户勾选了不再询问跳到设置页面 Intent intent new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse(package: getPackageName())); startActivity(intent); } } } }逻辑说明shouldShowRequestPermissionRationale是判断用户意图的关键。首次请求被拒绝时它返回 true可以弹自定义提示说明为什么需要权限用户勾选「不再询问」后它返回 false这时弹窗无效只能跳转设置页面。5.4 播放中断场景回归来电、耳机拔出、导航播报一份作业能不能从「做完」变成「做完且稳」看的是异常场景。我会在自己的检查清单里过这几项来电时看是否自动暂停。这实际上由 AudioFocus 机制接管上一节里的focusChangeListener在收到AUDIOFOCUS_LOSS_TRANSIENT时会暂停播放挂断后收到AUDIOFOCUS_GAIN时恢复播放。拔出耳机时是否静音。Android 通过ACTION_AUDIO_BECOMING_NOISY广播通知音频输出设备发生变更。监听这个广播并暂停播放是专业播放器的常规操作IntentFilter filter new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(noisyReceiver, filter);设计逻辑该广播是 sticky 广播吗不是所以必须在动态注册的情况下才能收到manifest 静态注册是无效的。5.5 高分答辩加分项导出时的打包与授权到了演示阶段把 App 装到手机上跑通只是第一步导出 APK 时反而容易在签名上翻车。Android Studio 的Build - Generate Signed Bundle / APK需要先创建一个.jks密钥库位置建议不要放在项目目录下防止误提交到 Git 仓库。build.gradle里签名配置写作android { signingConfigs { release { storeFile file(release.jks) storePassword your_password keyAlias your_alias keyPassword your_password } } buildTypes { release { minifyEnabled false signingConfig signingConfigs.release } } }逻辑说明minifyEnabled false表示不启用代码混淆期末演示阶段不需要为此增加变量。如果讲师要求查看 Logcat 调式信息debug包默认不签名直接Run即可但要提醒的是 debug 包和 release 包签名不一致导致真机升级安装时报签名冲突卸载旧包再装新的即可。到这里你已经拥有一个可以后台播放、带通知栏控制、音频焦点正确处理、权限逻辑完整、可安装到真机的音乐播放器 App——这套实现从架构上说和市面上大多数轻量播放器的工程骨架一致剩下的界面美化、歌词展示、播放模式切换本质都是在这套骨架上继续叠加。答辩时把「为什么用 Service」「为什么申请前台服务」「音频焦点如何处理」这三个问题讲透分数自然不会低。本文还有配套的精品资源点击获取
