拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android音乐播放器开发实战:适配分区存储与前台服务

简介本资源是一个基于Android Studio开发的完整音乐播放器项目面向Android初学者与进阶开发者聚焦多媒体应用开发核心能力训练涵盖UI设计、MediaPlayer音频控制、SQLite用户登录注册、背景动态切换及权限管理等实战要点。压缩包共712个文件主体为98个XML布局文件、161个Flat资源、122个JSON配置、37个PNG图标及16个Java源码辅以Gradle构建脚本、APK安装包和调试所需class/dex文件整体大小44.15MB结构清晰模块划分明确便于逐层理解播放器架构。已有7398人学习下载项目代码规范、注释充分包含从基础播放控制播放/暂停/上下曲到数据持久化SharedPreferences、SQLite、异步处理避免主线程阻塞及现代组件如ExoPlayer扩展提示的完整演进路径是掌握Android音视频开发全流程的优质实践范例。1. 为什么一个「Android音乐播放器」项目是检验 Android Studio 工程能力的黄金标尺不是所有带 UI 的 App 都能暴露真实开发水位。一个看似简单的「Android音乐播放器」恰恰在 Android Studio 环境下集中了从基础到进阶的完整技术断面它必须处理外部存储权限动态申请、MediaStore 或 MediaMetadataRetriever 的音频元数据解析、前台 Service 保活与通知栏控制、AudioManager 音量与焦点管理、SeekBar 拖动与进度同步、后台播放状态持久化还要应对 Android 10 分区存储Scoped Storage的适配断层。很多开发者卡在「能跑 Demo 却无法上线」问题往往出在 Android Studio 的 Gradle 构建配置、AGP 版本兼容性、模拟器音频设备支持缺失或真机调试时未启用 USB 调试文件传输模式。本文不讲空泛架构只聚焦你在 Android Studio 2022.3.1Chipmunk或更高版本中从新建项目到真机稳定播放 MP3/WAV 的可验证路径——包括build.gradle中必须改的 3 处 AGP 兼容配置、AndroidManifest.xml里被忽略的uses-permission组合、以及用adb shell快速验证音频文件是否被系统媒体库索引的命令。适合刚完成「Hello World」但还没独立完成过媒体类 App 的 Android 开发者也适合想排查「为什么模拟器没声音」「为什么小米手机点播放没反应」的老手。2. 在 Android Studio 中初始化项目避开 AGP 8.0 与 Scoped Storage 的双重陷阱2.1 创建最小可行项目结构选 Empty Activity 还是 Media Player ActivityAndroid Studio 新建项目时模板选择直接影响后续工作量。不要选「Media Player Activity」模板——它基于已废弃的VideoView和硬编码路径且默认未适配 Android 12 的foregroundServiceType强制声明。正确做法是选择Empty Activity最低 API 级别设为 21即 Android 5.0勾选Include Kotlin supportKotlin 在协程处理异步媒体加载时更简洁取消勾选Use androidx.artifacts*新版 Android Studio 默认已启用勾选反而可能触发旧依赖冲突提示若创建后发现gradle.properties中含android.useAndroidXtrue和android.enableJetifiertrue说明已正确启用 AndroidX若缺失需手动添加并同步项目。2.2 修改 build.gradleModule: app解决 AGP 8.0 构建失败的核心三处AGPAndroid Gradle Plugin8.0 对 Java 17 语法和依赖校验更严格。常见报错如tag number over 30 is not supported或Could not resolve com.android.tools.build:gradle本质是版本链断裂。以下是app/build.gradle中必须调整的三处plugins { id com.android.application id org.jetbrains.kotlin.android version 1.9.0 apply false // 注意此处是插件版本非 Kotlin 编译器版本 } android { namespace com.example.musicplayer // 必须与包名一致影响 R 文件生成 compileSdk 34 // 推荐使用最新稳定版避免 MediaSessionCompat 兼容问题 defaultConfig { applicationId com.example.musicplayer minSdk 21 // 若需支持 Android 10 分区存储minSdk 至少为 29 targetSdk 34 // targetSdk 33 必须声明 foregroundServiceType versionCode 1 versionName 1.0 testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner } buildTypes { release { minifyEnabled false // 初期关闭混淆避免 MediaPlayer 相关类被误删 proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 // AGP 8.0 强制要求 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget 17 } // 关键为 Android 12 添加前台服务类型声明 buildFeatures { viewBinding true } }参数说明namespace必须与AndroidManifest.xml中的package属性完全一致否则R类无法生成findViewById报红minSdk 21保证MediaPlayer基础 API 可用若需使用MediaSession控制锁屏播放则minSdk建议 ≥26targetSdk 34启用MediaSession新特性但需在AndroidManifest.xml中为 Service 声明android:foregroundServiceTypemediaPlaybacksourceCompatibility JavaVersion.VERSION_17AGP 8.0 不再支持 Java 11未修改将导致Gradle sync failed2.3 配置 AndroidManifest.xml权限、组件与服务声明的精确组合仅声明READ_EXTERNAL_STORAGE不足以在 Android 11 读取音频文件。必须按目标 SDK 分层处理manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.musicplayer !-- Android 10 及以下传统存储权限 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- Android 11分区存储下访问媒体文件需此权限 -- uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / !-- 前台服务必备权限 -- uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / !-- 音频焦点与音量控制 -- uses-permission android:nameandroid.permission.MODIFY_AUDIO_SETTINGS / application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name android:themestyle/Theme.MusicPlayer activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity !-- 播放服务必须声明 foregroundServiceType -- service android:name.MusicService android:enabledtrue android:exportedfalse android:foregroundServiceTypemediaPlayback / !-- Android 12 强制要求 -- !-- 媒体会话服务 -- service android:name.MediaSessionService android:exportedtrue android:permissionandroid.permission.MEDIA_CONTENT_CONTROL intent-filter action android:nameandroid.media.browse.MediaBrowserService / /intent-filter /service /application /manifest权限逻辑说明权限适用 Android 版本作用是否必需READ_EXTERNAL_STORAGE≤ Android 9读取外部存储根目录否Android 10 应用私有目录无需此权限READ_MEDIA_AUDIO≥ Android 11访问系统媒体库中的音频文件是替代旧权限FOREGROUND_SERVICE所有版本允许启动前台服务是播放必须MODIFY_AUDIO_SETTINGS所有版本调节音量、获取音频焦点是避免播放时被电话中断注意READ_MEDIA_AUDIO权限在 Android 13API 33后需用户手动授权首次调用ActivityCompat.requestPermissions()时弹窗标题为「允许访问音频文件」而非旧版「存储权限」。3. 实现核心播放逻辑从文件扫描到 MediaPlayer 控制的完整链路3.1 扫描设备音频文件绕过 Android 10 分区存储限制的两种可靠方式Android 10 引入分区存储后直接new File(/sdcard/Music/)会返回空列表。必须通过MediaStore查询系统媒体库或使用Storage Access Framework (SAF)让用户手动选择目录。推荐优先使用 MediaStore因其无需用户交互且兼容性好。方式一MediaStore 查询推荐适用于已导入媒体库的文件// MainActivity.kt private fun scanAudioFiles() { val projection arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.DATA // 文件绝对路径Android 10 仅对自身应用有效 ) val selection ${MediaStore.Audio.Media.IS_MUSIC} ? AND ${MediaStore.Audio.Media.DURATION} ? val selectionArgs arrayOf(1, 10000) // 时长 10 秒 val sortOrder ${MediaStore.Audio.Media.TITLE} ASC contentResolver.query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, sortOrder )?.use { cursor - val idColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) val titleColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE) val artistColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST) val durationColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION) val dataColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA) while (cursor.moveToNext()) { val id cursor.getLong(idColumn) val title cursor.getString(titleColumn) ?: Unknown val artist cursor.getString(artistColumn) ?: Unknown val duration cursor.getInt(durationColumn) val path cursor.getString(dataColumn) ?: // 构建 Content URIAndroid 10 安全访问方式 val contentUri ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id ) val song Song( id id, title title, artist artist, duration duration, uri contentUri // 用 Content URI 替代 file:// 路径 ) songList.add(song) } } }方式二Storage Access FrameworkSAF手动选择适用于未被媒体库索引的文件当MediaStore返回空时引导用户选择目录private fun openDocumentTree() { val intent Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode REQUEST_CODE_OPEN_DIRECTORY resultCode Activity.RESULT_OK) { data?.data?.let { treeUri - // 获取持久 URI 权限 contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) // 递归扫描该目录下所有 .mp3/.wav 文件 scanDirectory(treeUri) } } }3.2 构建 MediaPlayer 实例避免内存泄漏与状态错乱的关键步骤直接在 Activity 中 new MediaPlayer 易导致横竖屏重建时重复创建。必须将 MediaPlayer 封装进 Service并通过MediaSession统一控制。以下是MusicService.kt的核心骨架class MusicService : Service() { private lateinit var mediaPlayer: MediaPlayer private lateinit var mediaSession: MediaSession private var currentSong: Song? null override fun onCreate() { super.onCreate() initMediaPlayer() initMediaSession() } private fun initMediaPlayer() { mediaPlayer MediaPlayer().apply { setOnPreparedListener { // 准备就绪开始播放 start() updateNotification() sendBroadcast(Intent(ACTION_PLAYING)) } setOnCompletionListener { // 播放完成自动下一首 playNext() } setOnErrorListener { _, _, _ - // 错误处理释放资源并重置状态 stop() reset() returnsetOnErrorListener true } } } private fun initMediaSession() { mediaSession MediaSession(this, MusicService).apply { setCallback(object : MediaSession.Callback() { override fun onPlay() { if (!mediaPlayer.isPlaying) mediaPlayer.start() } override fun onPause() { if (mediaPlayer.isPlaying) mediaPlayer.pause() } override fun onSkipToNext() { playNext() } }) isActive true } } fun playSong(song: Song) { try { mediaPlayer.reset() // 使用 Content URI 打开音频适配分区存储 mediaPlayer.setDataSource(this, song.uri) mediaPlayer.prepareAsync() // 异步准备避免 ANR currentSong song } catch (e: Exception) { Log.e(MusicService, Failed to play song, e) } } private fun playNext() { // 实现下一首逻辑更新 currentSong调用 playSong() } private fun updateNotification() { // 构建 Notification显示播放控件 val notification NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(currentSong?.title) .setContentText(currentSong?.artist) .setSmallIcon(R.drawable.ic_play) .addAction(R.drawable.ic_pause, Pause, getPausePendingIntent()) .build() startForeground(NOTIFICATION_ID, notification) } }关键参数说明setDataSource(this, song.uri)传入ContentUri而非File路径确保 Android 10 可读prepareAsync()必须异步调用否则主线程阻塞超 5 秒触发 ANRsetOnCompletionListener监听播放结束实现自动连播startForeground()前台服务必须调用否则 Android 8.0 会立即杀死服务3.3 UI 层绑定用 ViewBinding SeekBar 实现精准进度控制在activity_main.xml中定义播放控制区域LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding16dp TextView android:idid/tv_title android:layout_widthwrap_content android:layout_heightwrap_content android:textSize18sp / SeekBar android:idid/seek_bar android:layout_widthmatch_parent android:layout_heightwrap_content / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:gravitycenter ImageButton android:idid/btn_prev android:layout_width48dp android:layout_height48dp android:srcdrawable/ic_skip_previous / ImageButton android:idid/btn_play_pause android:layout_width48dp android:layout_height48dp android:srcdrawable/ic_play / ImageButton android:idid/btn_next android:layout_width48dp android:layout_height48dp android:srcdrawable/ic_skip_next / /LinearLayout /LinearLayout在MainActivity.kt中绑定事件private fun setupUI() { binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { if (fromUser ::musicService.isInitialized) { // 用户拖动时跳转到指定位置 musicService.seekTo(progress) } } override fun onStartTrackingTouch(seekBar: SeekBar?) {} override fun onStopTrackingTouch(seekBar: SeekBar?) {} }) binding.btnPlayPause.setOnClickListener { if (musicService.isPlaying()) { musicService.pause() binding.btnPlayPause.setImageResource(R.drawable.ic_play) } else { musicService.play() binding.btnPlayPause.setImageResource(R.drawable.ic_pause) } } }提示SeekBar的max值需在MediaPlayer准备完成后设置binding.seekBar.max mediaPlayer.duration否则拖动无效。4. 真机调试与性能优化解决「小米/华为手机无声音」「进度条卡顿」等高频问题4.1 验证音频文件是否被系统媒体库索引用 adb shell 快速诊断很多「播放无声」问题源于文件未被MediaStore扫描。在 Android Studio Terminal 中执行# 查看媒体库中音频总数 adb shell content query --uri content://media/external/audio/media --projection title,artist,duration | head -n 10 # 强制刷新媒体库针对新拷贝的 MP3 文件 adb shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/Music/test.mp3若第一条命令返回空说明文件未入库第二条命令执行后等待 10 秒再运行第一条验证。注意file://路径在 Android 10 仅对应用私有目录有效公共目录需用content://URI。4.2 小米/华为手机真机调试专项配置小米手机常见问题USB 调试开启后仍无法安装 APK 或调试。必须进入「设置 → 我的设备 → 全部参数」连续点击「MIUI 版本」7 次开启开发者选项返回「设置 → 更多设置 → 开发者选项」开启「USB 调试」和「USB 调试安全设置」连接电脑后通知栏下拉选择「文件传输」模式非「仅充电」在 Android Studio 中Run → Select Device选择小米设备若显示「unauthorized」在手机上确认授权弹窗华为手机需额外开启「HDB 调试」「设置 → 系统和更新 → 开发人员选项」中开启「HDB 调试」在终端执行adb kill-server adb start-server重启服务4.3 优化 SeekBar 同步精度避免进度跳变与卡顿的 3 个参数默认MediaPlayer.getCurrentPosition()调用频率过高会导致主线程卡顿。采用 Handler 定时轮询并防抖private val handler Handler(Looper.getMainLooper()) private val updateProgressRunnable object : Runnable { override fun run() { if (::musicService.isInitialized musicService.isPlaying()) { val currentPosition musicService.currentPosition() val duration musicService.duration() if (duration 0) { // 防抖仅当进度变化超过 1 秒才更新 UI if (abs(currentPosition - lastUpdatePosition) 1000) { binding.seekBar.progress currentPosition lastUpdatePosition currentPosition } } } handler.postDelayed(this, 500) // 每 500ms 更新一次平衡精度与性能 } } // 启动轮询 handler.post(updateProgressRunnable)关键参数对比表参数值效果适用场景postDelayed(..., 100)100ms进度条极其顺滑但 CPU 占用高高端机、追求极致体验postDelayed(..., 500)500ms平衡精度与性能推荐值主流机型默认配置postDelayed(..., 1000)1000ms进度更新延迟明显但功耗最低低端机、后台播放4.4 解决「Android Studio 模拟器无声音」的终极方案Android Studio 自带模拟器AVD默认不启用音频硬件。必须在 AVD Manager 中编辑设备 → «Show Advanced Settings» → 勾选Enable Audio Input和Enable Audio Output启动模拟器后在模拟器右侧面板点击«More» → Audio → Enable Audio»若仍无声在模拟器中进入「Settings → Sound」确认媒体音量未静音注意部分 Windows 系统需在「声音设置 → 输入设备」中选择「Microphone (Intel® Smart Sound Technology)」否则模拟器音频输入不可用。5. 进阶技巧用 MediaSession 兼容 Wear OS 与车机并实现锁屏控制5.1 构建 MediaSession 以支持跨设备控制MediaSession是 Android 媒体生态的中枢。在MusicService.onCreate()中补充private fun initMediaSession() { mediaSession MediaSession(this, MusicService).apply { setCallback(object : MediaSession.Callback() { override fun onPlay() { /* 启动播放 */ } override fun onPause() { /* 暂停播放 */ } override fun onSkipToNext() { /* 下一首 */ } override fun onSeekTo(pos: Long) { mediaPlayer.seekTo(pos.toInt()) // 注意pos 是 LongMediaPlayer 接受 Int } }) // 设置媒体元数据供锁屏和 Wear OS 显示 setMetadata( MediaMetadata.Builder() .putString(MediaMetadata.METADATA_KEY_TITLE, currentSong?.title) .putString(MediaMetadata.METADATA_KEY_ARTIST, currentSong?.artist) .putLong(MediaMetadata.METADATA_KEY_DURATION, currentSong?.duration?.toLong() ?: 0) .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, albumArtBitmap) .build() ) isActive true } }5.2 实现锁屏控制添加 Notification Action 并响应点击在updateNotification()中添加MediaStyleval notification NotificationCompat.Builder(this, CHANNEL_ID) .setStyle( androidx.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSession.sessionToken) // 关键绑定 MediaSession .setShowActionsInCompactView(0, 1, 2) // 在精简视图中显示前/播/后按钮 ) .addAction(R.drawable.ic_skip_previous, Previous, getPrevPendingIntent()) .addAction(R.drawable.ic_play, Play, getPlayPendingIntent()) .addAction(R.drawable.ic_skip_next, Next, getNextPendingIntent()) .build()5.3 测试 Wear OS 兼容性用 Android Studio 模拟器快速验证无需真机用 Android Studio 自带的 Wear OS 模拟器AVD Manager → «Create Virtual Device» → 选择 «Wear OS» 设备如 «Wear OS Square»系统镜像选择 «Latest Release»如 Android 13启动后在 Wear 模拟器中打开「Google Play 商店」→ 搜索 «Android Wear» → 安装配套 App在手机端启动音乐播放器Wear 设备会自动同步播放状态并显示控制按钮提示若 Wear 设备未显示控制界面检查手机端是否开启「设置 → 连接的设备 → Wear OS」中的「媒体控制」开关。至此你已在 Android Studio 中构建了一个符合现代 Android 规范、适配分区存储、支持真机与 Wear OS 的音乐播放器。下一步可扩展 Equalizer均衡器、歌词同步LRC 解析、或离线缓存Room 数据库存储播放历史。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门