AI 辅助开发实录:一周做完一个 Android 本地音乐播放器,这些坑替你踩了

从 PRD 到上线:一个纯本地 Android 音乐播放器的完整开发复盘(Kotlin + Media3)

本文是个人项目的开发复盘。项目是一个纯本地、不联网、不上传的安卓音乐播放器,从写 PRD、任务拆解到 v1.0 → v1.3 四个版本迭代,全部用「AI 辅助开发 + 真机自测」完成。文中所有账号、路径、签名信息已脱敏。

一、为什么要做这个项目

市面上播放器要么塞满广告和推荐流,要么强依赖网络。我想要的是一个非常干净的工具:

  • 打开就能看到本地音乐,点开就播
  • 锁屏、通知栏、耳机线控都能控制
  • 没有网络权限------不可能偷偷上传任何东西

于是先写了 PRD(15 段结构、FR-01~17 逐条编号 + GWT 验收标准),再拆解技术任务,然后按版本迭代。事实证明这套「先文档后编码」的流程哪怕对个人小项目也非常值得------后面会讲为什么。

技术选型 :Kotlin + Jetpack Media3(ExoPlayer + MediaSession)+ Room,minSdk 33(Android 13,用 READ_MEDIA_AUDIO 细粒度权限)。

二、版本规划:小步快跑

版本 范围 核心价值
v1.0 扫描 + 播放 + 后台 + 系统控制 + 搜索排序 基础闭环
v1.1 循环/单曲/随机 + 自建播放列表(Room) 高频基础能力
v1.2 本地歌词(lrc + 内嵌)+ 均衡器 体验增强
v1.3 专辑/歌手视图 + 倍速 + 睡眠定时器 日常刚需

每个版本都是「规划锁定 → 开发 → 真机自测清单逐项打勾」,不写需求边界模糊的代码。

三、整体架构

scss 复制代码
┌─ UI 层 ──────────────────────────────┐
│ MainActivity(权限+导航)               │
│ SongsFragment / PlayerActivity       │
│ PlaylistsFragment / SettingsFragment │
└──────────────┬───────────────────────┘
               │ MediaController(跨进程会话客户端)
┌──────────────▼───────────────────────┐
│ PlaybackService(MediaSessionService)│
│  ├ ExoPlayer(焦点/耳机拔出/倍速)     │
│  ├ MediaSession(通知栏/锁屏/线控)    │
│  └ EffectsManager(Equalizer)        │
└──────────────┬───────────────────────┘
┌──────────────▼───────────────────────┐
│ 数据层                                │
│  ├ MusicRepository(MediaStore+缓存) │
│  ├ Room(playlist / playlist_item)   │
│  └ LrcParser / LyricsProvider / Prefs │
└───────────────────────────────────────┘

关键决策:UI 层只和 PlayerClient(MediaController 封装)说话,永远不直接碰 ExoPlayer。这样 Activity 怎么生生死死,后台播放都不受影响,而且通知栏/锁屏控制天然免费。

四、核心实现拆解

1. 媒体扫描:MediaStore + 缓存 + 增量刷新

只查 IS_MUSIC != 0 的条目,首启先读私有目录的 JSON 缓存(<1s 出列表),后台静默全量校验,再注册 ContentObserver 监听媒体库变化做增量刷新:

kotlin 复制代码
app.contentResolver.registerContentObserver(
    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true,
    object : ContentObserver(handler) {
        override fun onChange(selfChange: Boolean, uri: Uri?) {
            handler.removeCallbacks(pending)      // 3s 防抖
            handler.postDelayed(pending, 3000)
        }
    }
)

2. 后台播放:Media3 把最难的部分全包了

前台服务 + 通知栏 + 锁屏 + 耳机线控 + 音频焦点,这些「系统级脏活」Media3 全部自动处理,真正要写的只有配置:

kotlin 复制代码
val player = ExoPlayer.Builder(this)
    .setAudioAttributes(attrs, /* handleAudioFocus = */ true) // 焦点自动抢占/恢复
    .setHandleAudioBecomingNoisy(true)   // 耳机拔出自动暂停
    .setWakeMode(C.WAKE_MODE_LOCAL)      // 播放时保持 CPU 唤醒
    .build()
mediaSession = MediaSession.Builder(this, player).build()

代价是三行配置对应三个必踩的坑,见下文踩坑记录。

3. 播放模式:把状态机抽成纯函数

顺序/列表循环/单曲循环直接映射 ExoPlayer 的 repeatMode,Shuffle 用 shuffleModeEnabled。但「4 模式 × Shuffle」组合的下一首逻辑容易写崩,所以抽了个纯函数对象做回归测试(8 个组合用例):

kotlin 复制代码
fun computeNext(index: Int, size: Int, repeatMode: Int,
                shuffle: Boolean, playedInRound: MutableSet<Int>): Int

运行时由 ExoPlayer 承载,纯函数版作为同口径参考实现跑单测------改逻辑先跑测试,不用每次装真机。

4. 播放列表:song_key 而不是 URI

Room 两张表 playlist / playlist_item,条目存的是 MediaStore _id(song_key)而不是 content URI------文件被移动/重建索引后 URI 会失效,_id 稳定得多,配合「失效项标记 + 一键清理」兜底。

5. 歌词:本地 lrc + 内嵌,全容错

  • 同目录同名 .lrc(多个冲突取修改时间最新的)
  • 内嵌歌词字段是 @hide 常量,公开 SDK 引用不到,用反射拿值、拿不到就当没有
  • 编码探测:BOM → UTF-8 → 出现乱码替换符回退 GBK
  • 时间轴容错:排序 + 去重 + 负值丢弃,解析挂了就降级「暂无歌词」,绝不影响播放

6. 均衡器:绑定 audioSessionId + 能力降级

kotlin 复制代码
// 播放会话建立时
Equalizer(0, player.audioSessionId)
// 会话变化时
override fun onAudioSessionIdChanged(id: Int) { EffectsManager.attach(id, prefs) }
// 服务销毁时必须 release,避免音频资源泄漏

预设统一用增益数组实现(按索引重采样到设备实际频段数),不依赖厂商内置预设名------不同 ROM 内置预设列表差异很大。

五、踩坑记录(本文最有价值的部分)

以下坑全部真实发生,按发现顺序排列。

坑 1:镜像源配了不生效,还在从官方下载

gradle-wrapper.properties 改成了国内镜像,构建时却还在下载官方 -src.zip。排查发现是 IDE 用自己的引导逻辑接管了 wrapper (项目缺 gradle-wrapper.jar 时)。教训:wrapper 相关问题先确认「这次构建到底读的是哪份配置、哪个 jar」。

坑 2:Invalid token LOCALIZED ------ 查询排序直接崩溃

arduino 复制代码
// ❌ MediaProvider 不认识这个 token,整个查询抛 IllegalArgumentException
"TITLE COLLATE LOCALIZED ASC"
// ✅ 普通 ASC,拼音排序放内存里用 Collator 做
"TITLE ASC"

MediaStore 查询走 ContentProvider 到系统服务端执行,服务端 SQLite 不支持本地化排序器。排序需求在客户端做,查询里只写最朴素的 SQL。

坑 3:点击播放秒崩 SecurityException: WAKE_LOCK

用了 setWakeMode(C.WAKE_MODE_LOCAL) 却没在 Manifest 声明 android.permission.WAKE_LOCK。这是 ExoPlayer 文档明确要求的配套权限,普通权限、安装即授予,但漏了就是真机上一触即崩。

坑 4:style 里没写 layout_width,inflate 直接炸

多个控件只写了 style="@style/SettingRow",style 里没定义布局尺寸,运行时 You must supply a layout_width attribute。修法是在 style 中补 android:layout_width/height(style 携带 layout 属性是生效的),一次修所有引用处。

坑 5:用 visibility 状态当业务标志位,首页一片黑

防重复初始化写成了 if (contentGroup.visibility == VISIBLE) return,但该容器在 XML 里默认就是 VISIBLE ,首次启动直接短路返回,Fragment 根本没被添加。视图状态 ≠ 业务状态,业务状态用显式布尔字段。

坑 6:lifecycleScope.launch { } 里的 this 不是 Activity

launch 的块接收者是 CoroutineScope,嵌套 lambda 里裸写 this 拿到的是 Scope 而不是 Context,Toast.makeText(this, ...) 编译都过不了。协程作用域内一律用 this@Activity 或 applicationContext。

坑 7:厂商 ROM 的音效能力探测失败

在部分 ROM 上,未播放时用临时 Equalizer(0, 0) 探测能力会被 AudioFlinger 拒绝。正确姿势是把「创建失败」当成正常的探测结果:降级为隐藏音效入口 + 提示「设备不支持」,播放会话建立后再重新绑定。

坑 8:日志沉默的 bug 最难查

扫描为空时界面无任何反馈、logcat 一片安静。后来给整条链路加了分层数量诊断日志(total_audio / is_music / filtered_rows),配合手动重扫按钮 + Toast 反馈,一分钟定位到坑 2。自用项目也要在关键路径埋「分层数据日志」,出问题时能立刻定位是权限、数据源还是过滤条件的问题。

六、经验总结

  1. 先写 PRD 再写代码:FR 编号 + GWT 验收标准让每个功能都有明确的「完成定义」,AI 辅助生成代码时也极大地减少了沟通损耗;
  2. 权限最小化是卖点也是约束 :只有 READ_MEDIA_AUDIO + 前台服务 + 通知 + WAKE_LOCK,每个权限都能说出用途;
  3. 系统级能力交给系统框架:MediaSession 生态(通知/锁屏/线控/焦点/蓝牙)自研成本极高且永远有兼容性长尾;
  4. 真机自测清单逐项打勾:每版一份,MUUI/国产 ROM 的行为差异基本都在清单里暴露;
  5. 统计只存本地不上传:本地日志文件 + 设置页查看,既满足隐私洁癖又服务了排障。

七、结语

这个项目从 PRD 定稿到四个版本功能全部跑通,累计不到一周的业余时间。最大的感受是:现代 Android 开发的「地基」已经非常厚了------Media3 把播放器最难的部分标准化,剩下的其实是把需求拆清楚、把每个坑都真机踩一遍。

如果这篇文章对你有帮助,欢迎点赞收藏;踩坑列表如果有你遇到过的同款,评论区聊聊。


相关推荐
hai_android1 小时前
InputStage 责任链核心执行流程
android·android studio·android jetpack
mmsx4 小时前
Android 测绘计算工程化:七四参数、高程拟合与单位系统
android·kotlin
我是场6 小时前
我的NPI项目之-Touch的EE特性
android·arm开发
天空之城--6 小时前
Android一周动态:AI编程、Compose与车载生态新信号
android·flutter
恋猫de小郭7 小时前
Dart 4.0 要彻底移除 dart:mirrors,Augmentations 应该要来了
android·前端·flutter
Carson带你学Android9 小时前
Gemini 4 Argon 发布:对 Android 开发者意味着什么?
android·ai编程
事圆则缓9 小时前
Kotlin Flow、StateFlow、SharedFlow 全解析:冷流、热流与 Android 状态管理
android·kotlin·php
应用市场10 小时前
把旧 Pixel 变成相册备份中转站(上):Mac 到安卓的照片传输工具设计——流式上传、sha256 校验、adb forward 与 Bonjour
android·macos·adb·kotlin·swift
墨天梦10 小时前
B08_Fragment事务通信与双生命周期
android