版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接 🦉
阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉
废话不多说,先来一波截图纯享。
| 标题 | ||
|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
一、动因
两年前写上篇文章的时候,啵啵音乐刚过 1.0。之后一路迭代,今年五月更到 2.7.0,然后就停更了。
不是不想动,是不敢动。
这个项目是我边学 flutter 边写的,第一版什么成色,我心里有数 ------ 能跑,但内里全是欠条。用得越久,欠条越多。随手列几条给大家品品:
1.缓存的歌整首驻留内存,播放进度靠 300ms 轮询刷
2.歌词时间轴的单位解析错了,滚动节奏永远对不上,比歌声慢十倍
3.启动的时候,一边把待播放列表清空,一边又去读它,主打一个精神分裂
4.SQL 里拼表名,删除逻辑宽匹配,一不小心就误删歌曲
5.event_bus 满天飞,UI 直接读数据库,同一个状态三处各存一份
6.任何状态一变,notifyListeners 全量通知,整棵组件树陪跑
这就是 2.x 的现状。功能都能用,我自己也一直在用,但每次想加个新功能,都得先在欠条堆里刨半天。
我也认真考虑过渐进重构,方案都写到 P5 了。写到一半发现不对劲:地基是歪的,装修救不了。旧代码没有任何测试保护,改一处崩三处,重构的成本不比重写低,收益还不如重写。
而且这次和两年前不一样了 ------ 两年前我是 flutter 新手,现在好歹算个熟手;另外,AI 参与开发的方式也完全成熟了(这个后面细说)。
于是在某个深夜,我把 master 冻结存档,切了个新分支,然后把 lib/ 下的代码基本删光了。
对,删光了。
二、决策
删代码一时爽,但有几个决策得先想清楚,不然就是自杀式重写。
1.留什么?
只留一样东西:B 站音源的 SDK。这是整个项目最值钱的资产 ------ WBI 签名、风控票据、playurl 解析,全是当初一点点逆向出来的心血,重造不起。接入前做了番改造:剥掉和 UI 的耦合、错误类型化,再补了一轮特征测试(签名黄金值、解析快照),保证改造前后行为分毫不差。
其余全部推倒。旧代码的去处是 git 历史和文档,不做长期双轨。
2.状态管理用什么?
这里要先给两年前吹过的牛磕一个:上篇文章结尾我说,"下次开发我必然用上 getx"。
这次食言了,用的是 Riverpod 3。
原因也想清楚了:getx 是把状态、路由、依赖注入塞进一个袋子的全家桶,爽是爽,但黑魔法太多。而 2.x 用 provider + event_bus 的组合把我坑惨了,我这次要的是"编译期就报错"和"精确刷新" ------ Riverpod 的 codegen 正好对症,某个歌单变了就失效那一个 provider,不再全量刷新整棵树。event_bus 直接禁用,跨模块通信只准走 provider 依赖图。
3.版本号怎么定?
旧版已经发到 2.7.0,重写版从 3.0.0 起步,分支就叫 v3,对得起"第三代"这个名字。
4.旧数据迁不迁?
纠结过,迁移方案都写好了。最后还是砍掉:新旧数据库的表结构完全不同,硬迁移很容易把旧坑带进新实现。2.7.0 留档在 master,新旧互不干扰。
三、设计
这次我学乖了,动手前先写了一份重写计划,把功能清单、数据模型、状态域、播放内核一条条定死,其中光功能语义就有九条决策。举个 🌰:
1.曲目和歌单成员是两个概念:一首歌全局只有一个身份,可以出现在 N 个列表里。"喜欢"、歌词、下载标记都是歌本身的属性,天然全局同步,不需要任何同步代码。你在我喜欢里点了心形,歌单里同一首歌自动是红的------不是做了同步,是它本来就只有一份。
2.播放队列独立于歌单:插队、追加、允许重复、可调序,跨启动保留。
3.本地歌曲的身份用内容指纹:拿"文件大小 + 时长 + 归一化的标题歌手"生成指纹当主键,重扫一百遍,你的喜欢和歌词也不会丢。
数据库就四张表,SQL 全参数化,Repository 是唯一允许碰 SQL 的层。UI 想读库?没门,走 provider。
播放内核是重头戏,2.x 那套"整首进内存 + 300ms 轮询"必须死。新的方案是一个自定义的代理音频源:
1.Range 请求直取 CDN,边下边播,顺序流边播边落盘,播完自动转正为缓存
2.缓存按歌曲身份做 key,LRU 淘汰,上限 2GB
3.主链挂了自动降级备用直链,15 秒没动静看门狗直接介入
4.断点续播:歌曲、进度、播放模式、队列全部落盘,冷启动恢复
调这块的时候踩了个很阴的坑:播放一直没声音,日志也不报错。排查半天发现是看门狗的 Timer 声明了 late 但没初始化,空指针异常被上层的 catch 吞掉了,流程悄悄断了,落盘一个 0 字节的流。从那以后我对 "静默的 catch" PTSD 了。
系统媒体这块换了路子。audio_service 用下来,媒体通知的样式定制被模板天花板死死挡住,想要的样子根本画不出来,最后干脆整个换掉,自研了一个轻量媒体中心:Kotlin 和 Swift 各写一层薄桥,通知自己画,锁屏、控制中心、耳机线控全接。想画成什么样就画成什么样,再也不用跟别人的抽象搏斗。
音源从单一 B 站扩到了五个:B 站、酷我、网易、QQ、千千,搜索页一键切换。歌词做成三级检索链:站内直连 → 开放歌词库 → 备选源,自动匹配不到就手动搜,还支持时间校准。
本地扫描也重做了:后台 isolate 扫描,进度走流式事件,页面上一边扫一边出结果。UI 上则定了一整套设计系统------颜色全部走 token,全项目禁止硬编码色值;背景是"极光玻璃"风,三颗光斑慢速漂移加呼吸脉动,用缓存图层加位移实现,合成器零重绘;空状态统一换成平面矢量插画。
四、聊聊这一周
实际的施工从删库那天算起,到打出正式包定版,一共七天,108 个 commit。
前面的方案设计和 UI 原型打磨不算在内------那部分花的时间比施工长。慢设计,快施工,这笔账是算得过来的。
这周的开发体验和两年前写 1.x 时完全是两个世界,最大的变量是 AI。
两年前的模式是:我边学边写,卡住了找 AI 救火,AI 给的代码经常跑不起来,因为它推荐的库基本都断更了。这次反过来:我先把架构决策写成文档,给 AI 立好规矩------跨模块只准走 provider、SQL 全参数化、颜色只准走 token、静态检查零 issue 加测试全绿才准交付------然后让 AI 按图施工,我做方案决策、代码评审和真机验收。
规矩之外还有记忆:每个模块踩过的坑、修过的 bug、模块之间的关系,都沉淀成文档,下次开工先读记忆再动手。等于给项目配了一个不会离职、还过目不忘的同事。
当然,AI 也不是银弹。播放内核这种高度依赖真机行为的部分,AI 写的代码必须一条条用真机验。比如 B 站视频失效返回 -404,这种只有线上才知道的事,文档里不会写,AI 也编不出来,只有真机能说话。
测试这次是当交付门槛用的:五源各配了假实现,播放引擎有替身,切歌策略和状态机时序都有矩阵测试,全部加起来一百七十多例。2.x 是零测试,所以它才敢腐化。
最后必须吐槽一下构建环境,这七天里最脏的活其实是它:
1.工具链整体升级,132 个依赖 major 升级,AGP 9、Gradle 9.3.1、JDK 17 一步到位
2.AGP 9 砍了 Jetifier,但一堆插件的 Kotlin 版本跟不上,连锁报错;某个知名分享插件的新版本在 AGP 9 下编译丢类,只能锁死在旧版本窗口
3.NDK 想钉旧版?直接被拒,必须 28.2
4.本机 JAVA_HOME 指着 JDK 8,只能在构建脚本里再钉一遍 17
5.首次构建各种下载超时,满屏报错看着像全完了,其实重试就好
这些坑没有一个是业务逻辑,但每一个都能让你卡一晚上。好在这次有 AI 陪着排错,两年前那种"环境问题耗三天"的剧情没有重演。
五、结语
一周时间,从删库到 3.0.0:五个音源、三级歌词链、自研的播放内核和系统媒体桥、一百七十多例测试。两年前"免费听歌、无广告"的初心没变,变的只是里子。
重写最大的感受是:欠的债总是要还的,早还早超生。删代码的那天,是真的爽。
想下载体验的朋友请访问 GitHub Releases:啵啵音乐。
想看源码的朋友请访问 github 仓库:bobomusic。喜欢的话给个 star,谢谢。
往期推荐
搭建一个快速开发油猴脚本的前端工程 18+ 👍🏻 28+ 💚
金九银十招聘季,IT 打工人,该怎么识别烂公司好公司? 70+ 👍🏻 80+ 💚
别人休息我努力,悄悄写个 cli 工具,必须提升效率,skr~ 60+ 👍🏻 110+ 💚
一文掌握 eslint,再也不怕项目报错 20+ 👍🏻 30+ 💚
开发一个 npm 库应该做哪些工程配置? 40+ 👍🏻 50+ 💚
分享我在前端学习与开发中用到的神仙网站和工具 40+ 👍🏻 110+ 💚
uniapp 踩坑记录(二) 130+ 👍🏻 150+ 💚
闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+ 👍🏻 110+ 💚
uniapp 初体验踩坑记录 30+ 👍🏻 60+ 💚





