Android音乐播放器源码运行全攻略:MediaPlayer与ExoPlayer实战排错
2026/9/16 8:56:13 网站建设 项目流程

简介:基于Android Studio开发的音乐播放器APP源码,面向Android初学者、毕业设计及期末大作业学生。项目内含完整工程目录、构建脚本与可直接安装运行的APK文件,能够帮助读者理解从界面搭建到多媒体播放的完整实现流程,并掌握Android项目的导入、编译与调试方法。压缩包共375个文件,以Java源码(116个)和XML界面布局(74个)为核心,逻辑代码负责业务处理,布局文件定义页面结构;另含161张PNG格式图片素材、多个Gradle构建文件、APK安装包以及配置文件等,整体仅4.06MB,轻量紧凑,便于逐层阅读与复用。目前已有201人学习下载,适合作为课程设计参考或入门练手项目。阅读源码可学习Activity/Fragment页面组织、多媒体播放器调用、列表数据展示、构建调试与APK打包等关键技能,也可以借鉴其资源划分、命名规范和界面设计方式,为独立开发音乐类应用积累实战经验。

1. 拿到一份 Android 音乐播放器源码,你真正要解决的是什么

音乐播放器是 Android 开发里最容易被低估的项目类型。表面看是一个列表加一个播放按钮,实际上 MediaPlayer 状态机、音频焦点、前台服务、通知栏控制、切歌时的缓冲策略,每一层都能把刚入门的开发者按在地上摩擦。如果你下载了一份“基于 Android Studio 开发的音乐播放器 APP 源码”,大概率遇到的情况是:代码能打开但跑不起来,Gradle 同步卡在下载依赖,或者模拟器上播放没声音——这些都不是源码本身的 bug,而是你还没把工程环境、SDK 版本和播放架构三者对齐。

本文从一份常见音乐播放器源码出发,按“工程搭建 → 播放核心 → 后台与焦点 → 排错技巧”的顺序,把 Android Studio 里从源码到可运行 APK 的完整链路拆开讲清楚。适合正在做毕业设计、个人练手项目,或者准备把开源播放器改造成自己产品的开发者。读完你至少能回答三个问题:MediaPlayer 和 ExoPlayer 到底选哪个、音频焦点怎么处理才不闪退、通知栏上的播放按钮为什么点了没反应。

2. Android Studio 里搭建音乐播放器工程:Gradle 版本选型与依赖落地

2.1 先定 AGP 与 Gradle 的兼容关系,而不是盲目升级

任何一份源码跑不起来的首要原因,九成是 Android Gradle Plugin(AGP)版本和 Gradle 发行版不匹配。音乐播放器项目通常涉及 MediaPlayer、Service、RecyclerView,这些组件的 API 在 AndroidX 下相对稳定,但 AGP 版本决定了你能否顺利编译。常见的做法是先看项目里的build.gradle文件,找到com.android.application后面的版本号,再对应去查 AGP 和 Gradle 的官方兼容表。

AGP 版本最低 Gradle 版本推荐 JDK 版本
7.4.27.5JDK 11
8.0.08.0JDK 17
8.1.08.0JDK 17
8.2.08.2JDK 17

如果你下载的源码用的 AGP 是 7.4.2,却用 Android Studio 最新版默认创建的 Gradle 8.2 去打开,大概率会碰到Minimum supported Gradle version is 7.5的报错。反过来,AGP 8.0 的项目如果配 Gradle 7.4,会直接编译失败。我一般会在gradle-wrapper.properties里检查distributionUrl,确认它和 AGP 版本匹配后再做任何改动。

另一个常见坑是 JDK 版本。AGP 8.0 以后强制要求 JDK 17,而 Android Studio 自带的 JBR(JetBrains Runtime)通常是 11 或 17。如果源码注释里要求 JDK 11,但你的系统 JDK 是 8,编译时会出现Unsupported class file major version的报错。解决办法是在File > Project Structure > SDK Location里指定 Gradle JVM 为项目的 JDK 目录。

2.2 最小工程配置与依赖清单

一份可运行的音乐播放器源码,app/build.gradle里的核心配置大致如下。注意compileSdktargetSdk的取值,高版本 SDK 下部分权限行为有变化,后面排错章节会细说。

android { namespace 'com.example.musicplayer' compileSdk 34 defaultConfig { applicationId "com.example.musicplayer" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.media:media:1.7.0' }

这里最容易被忽略的是androidx.media依赖。如果你在源码里看到MediaSessionCompatPlaybackStateCompat这类类,而依赖里只有appcompat,编译时会出现Cannot resolve symbol MediaSessionCompat。这个包就是专门为音乐播放器提供媒体会话支持的。

compileSdk 34对应的targetSdk 34有一个重要变化:前台服务类型必须声明。后面做通知栏播放控制时,如果没在AndroidManifest.xml里声明foregroundServiceType="mediaPlayback",Android 14 真机上会直接抛出ForegroundServiceStartNotAllowedException

2.3 仓库镜像与首次同步,卡住时看这里

源码打开后第一步永远是 Gradle Sync。国内网络环境下,google()mavenCentral()仓库的访问速度不稳定是常态,音乐播放器项目里的mediaexoplayer依赖体积不小,经常同步到一半卡死。我一般在项目级的build.gradle里配置阿里云镜像仓库:

buildscript { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } google() mavenCentral() } } allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/public' } google() mavenCentral() } }

public仓库是 JCenter 和 Maven Central 的聚合,旧项目里的库基本都能拉到。gradle-plugin仓库专门放 AGP 插件本体,不加这个有时候会提示找不到com.android.application插件。同步速度慢还有一个隐蔽因素:Gradle 本身的分发下载。如果gradle-wrapper.properties里的distributionUrl指向services.gradle.org,第一次同步会额外下载一个 130MB 左右的压缩包。

提示:将distributionUrl改为腾讯或华为的镜像地址,可以显著缩短首次同步时间。改动后重启 Android Studio 再 Sync 一次即可。

3. 播放核心:MediaPlayer 与 ExoPlayer 的实现路径对比

3.1 选 MediaPlayer 还是 ExoPlayer:技术债和灵活度的权衡

音乐播放器源码里最常见的播放实现是MediaPlayer,因为它内置在 Android 框架里,不需要额外引入依赖。但 MediaPlayer 的弱点也很明确:不支持 HLS 流媒体、错误处理粗糙、音频格式扩展需要依赖底层系统编解码器。ExoPlayer 是 Google 官方推荐的替代方案,支持 DASH、HLS、SmoothStreaming,以及MediaSource的自由组合。

对比维度MediaPlayerExoPlayer
依赖方式系统内置implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
流媒体支持仅支持 HTTP 渐进式播放DASH / HLS / SmoothStreaming
音效增强支持均衡器,部分机型有差异支持AudioEffect,但需要自己接入
自定义能力扩展困难可自定义RenderersFactory
内存占用较低相对较高,多渲染器开销

如果你的源码项目里用的是 MediaPlayer,且只做本地音频播放,我建议保留现状,毕竟改造到 ExoPlayer 涉及 UI 层的播放状态同步,改动量大。如果源码里已经有SimpleExoPlayer的痕迹,那我更倾向完整切到 ExoPlayer,因为可维护性更好,MediaSession 的连接也更规范。

3.2 MediaPlayer 的最小播放流程与状态机避坑

MediaPlayer 是一个带状态机的类,很多人踩的坑就是没按状态流转图调用方法,导致触发IllegalStateException。核心路径是:Idle → Initialized → Prepared → Started → Paused → PlaybackCompleted

public class PlayerManager { private MediaPlayer mediaPlayer; public void play(String url) { if (mediaPlayer == null) { mediaPlayer = new MediaPlayer(); mediaPlayer.setOnCompletionListener(mp -> { mp.stop(); mp.reset(); // 这里要重新 prepare,不能直接 start }); mediaPlayer.setOnErrorListener((mp, what, extra) -> { mp.reset(); return true; // 消费错误,不会崩溃 }); } try { mediaPlayer.reset(); mediaPlayer.setDataSource(url); mediaPlayer.setAudioAttributes( new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ); mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(mp -> mp.start()); } catch (IOException e) { e.printStackTrace(); } } }

关键的点有两个。第一,prepareAsync()setOnPreparedListener必须配对使用,在onPrepared之前调用start()是无效的。第二,setAudioAttributes在 API 21 以后是强制要求,不设置的话播放时会没有声音,因为系统默认的 audio usage 不是USAGE_MEDIA。这是很多新手在模拟器上播放静音的根因。

reset()的作用是将播放器状态拉回 Idle,释放底层资源。每次切换歌曲前调用reset(),可以避免上一次的数据源残留影响下一次播放。这里有一个细节:reset()之后必须重新setDataSource,否则直接prepareAsync会报IllegalStateException

3.3 用 ExoPlayer 替换 MediaPlayer 时的核心代码差异

如果源码决定换 ExoPlayer,播放器初始化的方式完全不同。ExoPlayer 不用prepareAsync,而是通过setMediaSourceprepare的组合来启动。

val player = ExoPlayer.Builder(context) .setAudioAttributes( AudioAttributes.Builder() .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .setUsage(C.USAGE_MEDIA) .build(), true ) .setHandleAudioBecomingNoisy(true) .build() val mediaSource = ProgressiveMediaSource.Factory( DefaultDataSource.Factory(context) ).createMediaSource(MediaItem.fromUri(url)) player.setMediaSource(mediaSource) player.prepare() player.playWhenReady = true

setHandleAudioBecomingNoisy(true)是 ExoPlayer 处理插拔耳机场景的内置方案,当耳机拔出而手机外放时自动暂停播放。这在 MediaPlayer 时代需要自己注册AudioManager.ACTION_AUDIO_BECOMING_NOISY广播来实现,ExoPlayer 把这个逻辑收进了播放器内部,减少了很多样板代码。但需要注意的是,如果源码里有自己实现的耳机广播监听器,切到 ExoPlayer 后不会冲突,因为广播事件发生后两者都会收到。

playWhenReady这个参数在 ExoPlayer 里很容易被误解。它不是播放/暂停开关,而是表示“当准备好时是否立即播放”。区分这两个概念的关键是playbackState的状态机,暂停时应该调用player.pause()并把playWhenReady置为 false。

4. 让播放不被打断:音频焦点与通知栏前台服务

4.1 音频焦点机制:不只是调一个方法那么简单

音乐播放器 APP 最烦人 bug 之一是:切到其他应用刷视频,自己的播放还在继续,两个声音混在一起。出现这个问题的根因是没处理音频焦点(Audio Focus)。Android 的音频焦点机制是独占式的,播放器需要在开始播放前请求焦点,在失去焦点时暂停或降低音量。

AudioManager audioManager = (AudioManager) context.getSystemService(Context.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(this::handleFocusChange) .build(); int result = audioManager.requestAudioFocus(focusRequest); if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // 焦点请求被拒绝,不启动播放 }

AUDIOFOCUS_GAIN适合长时间播放的场景,比如音乐播放器;AUDIOFOCUS_GAIN_TRANSIENT适合短时播放,比如提示音。在回调里,AUDIOFOCUS_LOSS意味着永久失去焦点,应该暂停播放并释放焦点;AUDIOFOCUS_LOSS_TRANSIENT是临时失去,比如来电,应该暂停但不释放;AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK表示可以降低音量继续播放,比如地图导航播报。

很多源码项目只处理了AUDIOFOCUS_LOSS,忽略了CAN_DUCK分支,导致导航播报时音乐音量被完全切断而不是压低。正确的处理方式是:收到CAN_DUCK时调用mediaPlayer.setVolume(0.3f, 0.3f),收到AUDIOFOCUS_GAIN时恢复1.0f

从 API 26 开始,requestAudioFocus方法已经废弃,必须使用AudioFocusRequest.Builder创建请求。如果源码里出现audioManager.requestAudioFocus(listener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)这种写法,运行时不会崩溃,但会被 lint 标为 deprecated,且无法指定AudioAttributes

4.2 通知栏控制:前台服务与 MediaStyle 的结合

音乐播放器必须在后台持续播放,这就绕不开 Android 的后台限制。从 Android 8.0 开始,后台 Service 无法在应用退到后台时无限期运行,必须配合前台服务(Foreground Service)+ 通知栏。音乐播放器的通知栏需要显示歌曲信息、播放/暂停按钮、上一首/下一首按钮,这正是MediaStyle通知的作用。

val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_music_note) .setContentTitle(songTitle) .setContentText(artistName) .setContentIntent(pendingIntentMain) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setPriority(NotificationCompat.PRIORITY_LOW) .addAction(R.drawable.ic_prev, "上一首", prevPendingIntent) .addAction(R.drawable.ic_pause, "暂停", pausePendingIntent) .addAction(R.drawable.ic_next, "下一首", nextPendingIntent) .setStyle( androidx.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSessionCompat.sessionToken) .setShowActionsInCompactView(0, 1, 2) .setShowCancelButton(true) ) .build() startForeground(NOTIFICATION_ID, notification)

setShowActionsInCompactView(0, 1, 2)决定通知栏收起状态下显示哪几个操作按钮。这里的索引对应addAction的顺序,0 是上一首,1 是暂停,2 是下一首。如果addAction顺序调整了而这里没改,按钮就会出现错位或消失。

PendingIntent的撰写是另一个坑。播放/暂停按钮的点击事件返回当前 activity,不能简单getActivity,因为点击通知栏时 MainActivity 可能已经不在任务栈顶部。正确做法是:

val pauseIntent = Intent(context, PlaybackService::class.java).apply { action = ACTION_PAUSE } val pausePendingIntent = PendingIntent.getService( context, 1, pauseIntent, PendingIntent.FLAG_IMMUTABLE )

getService而不是getActivity来构造,配合 Service 的onStartCommand接收 action 并做对应操作。否则按钮点击没有任何反应,这是源码项目里最常见的 bug 之一。

PendingIntent.FLAG_IMMUTABLE是 Android 12 的强制要求。targetSdk 31 以上如果使用FLAG_MUTABLE,系统会直接抛异常。反过来,如果你需要往 Intent 里塞自定义数据让另一边读取,那就得用FLAG_MUTABLE,具体取决于用途。

4.3 Service 启动模式与播放服务的生命周期管理

后台播放服务需要保证只有一份实例在运行,否则会出现两首歌同时播放的诡异问题。在AndroidManifest.xml中声明 Service 时,我给播放服务设置了singleTop启动模式:

<service android:name=".playback.PlaybackService" android:enabled="true" android:exported="false" android:foregroundServiceType="mediaPlayback" android:launchMode="singleTop" />

launchMode="singleTop"的意思是,如果 Service 已经存在,onStartCommand依然会被调用,但不会创建新的实例。这一点在通知栏的上一首/下一首按钮频繁触发时至关重要。

onStartCommand的返回值也有讲究:

@Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); // 根据 action 分发:播放、暂停、上一首、下一首 } return START_NOT_STICKY; }

我选START_NOT_STICKY而不是START_STICKY,因为在播放器场景下,应用被系统杀掉之后,无脑重启 Service 反而会打断用户当前的音乐体验(比如用户只想听系统闹钟,结果你的播放器又自动播起来了)。但如果你做的产品需要在应用被杀后继续播放(比如白噪音应用),那START_STICKY是更合适的选择。

5. 从源码到稳定运行:Android 版本适配与播放器排错要点

5.1 高版本 Android 的权限与文件访问适配

源码里如果有从本地扫描音乐文件的功能,我在 Android 11(API 30)以上的设备上遇到过读不到文件的问题。这是因为 Android 11 引入了分区存储(Scoped Storage),READ_EXTERNAL_STORAGE权限不再直接授予对所有文件的访问权。

Android 版本权限声明实际效果
Android 10 及以下READ_EXTERNAL_STORAGE可读取所有媒体文件
Android 11 / 12READ_EXTERNAL_STORAGE只能读取媒体文件(通过 MediaStore),不能直接访问文件路径
Android 13 及以上READ_MEDIA_AUDIO需要单独申请音频权限,READ_EXTERNAL_STORAGE弃用

在 Android 13 以上,清单文件中要这么声明:

<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

并且运行时申请权限时,需要在代码里做一个版本判断:

if (Build.VERSION.SDK_INT >= 33) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_MEDIA_AUDIO}, REQ_CODE); } else if (Build.VERSION.SDK_INT >= 23) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQ_CODE); }

还有一种情况:源码里通过Environment.getExternalStorageDirectory()拼接文件路径来访问本地歌曲。这个做法在 targetSdk 30 以上基本已经失效,因为分区存储下拿到的/storage/emulated/0/路径没有直接读取权。常见做法是改用MediaStore查询,可以通过content://media/external/audio/media这个 URI 获取所有音频文件,配合MediaMetadataRetriever读取标题、歌手、封面信息。

5.2 播放卡顿与音画不同步的三个排查方向

音乐播放器出现卡顿,不要急着改代码,先判断卡顿发生在哪个阶段。我一般按这样的顺序排查:

  • 数据源阶段:本地播放卡顿,优先检查存储是否损坏;网络播放卡顿,检查MediaPlayersetBufferSize是否分配了足够的缓冲内存。
  • 解码阶段:高品质 FLAC 文件在低端机上解码压力大,用MediaCodec的降级方案,比如优先使用硬件解码器。
  • UI 更新阶段:播放进度条的更新循环如果是用Handler.postDelayed高频刷新,会持续占用主线程;点数高的歌曲列表同时刷封面图,也可能导致列表卡顿。

Handler刷新进度条的正确姿势:

private Runnable progressRunnable = new Runnable() { @Override public void run() { if (mediaPlayer != null && mediaPlayer.isPlaying()) { int position = mediaPlayer.getCurrentPosition(); progressBar.setProgress(position); handler.postDelayed(this, 500); } } };

注意postDelayed(this, 500)的 500ms 间隔是和真实播放毫秒数接受的延迟平衡,太短了界面闪烁,太长了进度跳跃。另外在onPauseonStop时务必handler.removeCallbacks(progressRunnable),否则 Activity 销毁后回调还在执行,会触发泄漏。

5.3 模拟器与真机的行为差异,以及快速验证方法

模拟器上能播放的音频,真机上不一定正常,反之亦然。模拟器没有真实的音频设备管线,某些机型模拟器上播放没问题,真机上听不到声音,或因音频焦点抢占被暂停。快速验证方式:

adb shell dumpsys audio | grep -A 10 "playback activity"

这个命令能列出当前音频焦点持有者是谁,以及各播放流的状态。执行结果如果显示AudioFocusHealth正常但播放器无声音,问题可能出在STREAM_MUSIC的音量被调成了静音:

adb shell media volume --stream 3 --set 15

还有一个我经常用的真机验证技巧:把播放器 App 切到后台,然后打开系统相机录像,观察背景音乐是否自动暂停。这套操作可以对音频焦点处理做端到端的测试,比单纯调试代码更高效。

广播注册的audio becoming noisy监听也高度关联音频焦点。当你拔掉耳机时,系统会发出一条ACTION_AUDIO_BECOMING_NOISY广播,只有动态注册的接收器能收到。在AndroidManifest里静态声明这个 receiver 是无效的,这是源码移植时特别容易忽略的点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询