用 Rust 重构开发安卓音乐播放器,我遇到了哪些坑?
一个基于开源 酷狗 API 的开源音乐播放器(Material Design 3),从 0 到发布 5.x版本,我踩过的那些真正的坑。先交代背景:这是一个开源项目 MD3Music,Flutter 写 UI,内嵌一个用 Rust 写的本地 API 服务器(App 内通过 JNI 启动,监听 `127.0.0.1`),所有酷狗音乐接口都先打到本地服务器、由它签名后转发到官方网关。断网也能浏览本地音乐,在线播放支持 128k / 320k / 无损 / Hi-Res,还有 Apple Music 风格逐字歌词。项目从 v5.0.0 开始做了一次大重构,一路迭代到现在。本文不讲"我怎么搭架构",只讲踩坑——尤其是那种排查到怀疑人生、最后发现是某个冷门细节的坑。坑 1:在线播放"卡顿 + 跳 10 秒",本地播放却完全正常
这是整个项目最折磨人的一个 bug,因为出现的的少,很隐蔽,少部分手机会触发,播放几首歌后就消失不见。症状:在线播歌时,音频会真实中断几秒,然后进度条凭空往前跳约 10 秒(反复出现,幅度在 9~11 秒波动,集中在歌曲开头)。本地文件播放一切正常,128kbps 的码率照样跳。最诡异的是:只要一开始录屏,它就恢复正常了。- 流本身脏 —— 用 Python 拉下来逐帧解析:7957 帧 CBR 128kbps,零空洞,Range 请求正确返回 206;
- 缓冲不足 —— 播放器状态全程
ready,从没进过buffering,说明数据早就到位了; - 外部设备 seek —— 排查了 Lyricon 歌词硬件推送,入站消息只处理连接状态;
- CPU 降频/睡眠 —— 自定义的
AudioPlaybackService 在播放时持有了PARTIAL_WAKE_LOCK(acquire(24h)),但这套 wakelock 方案是 no-op,白做; - 高码率解码压力 —— 128kbps 也跳,排除。
最后定位到根因:ExoPlayer 2.19 的渲染层/线程调度 bug,只在在线 HTTP 喂数的特定时序下被触发。升级到 Media3(`just_audio` 0.9.34 → 0.10.6)后根治,Dart API 完全兼容,零报错迁移。教训:`just_audio` 0.9.x 底层还是 ExoPlayer 2.19,0.10.x 已经切到 Media3。升级大版本依赖前,先读 changelog,别让旧的渲染层 bug 替你背黑锅。关于这个 bug 的完整排查过程,我单独写了一篇,见后文。坑 2:删歌单一直失败,响应体是一段 URL
"我的收藏"里删歌单,用户一直收到「删除失败,请稍后重试」。日志里 `/v2/delete_list` 返回 502,响应体却是 `{status: 0, msg: <完整请求URL>}`。这个特征我后来才看懂:`gateway.kugou.com` 是按 `x-router` 请求头把请求分发到后端服务的网关。缺了该头,网关没法路由,就返回 HTTP 200 + 一段回显 URL(本地服务器把它当成错误透传成了 502)。根因:Rust 服务器转发删除歌单请求时,少了 `x-router: cloudlist.service.kugou.com` 这个头。补上即好。同类接口要么带 `x-router` 头,要么把 host 直接写进 path(比如 `/cloudlist.service/v5/add_list`)。教训:`502` 不一定真是服务端挂了。看到"HTTP 200 + body 里是完整 URL",先怀疑网关路由,别急着查签名和鉴权。另外网关诊断回显会主动去掉 `signature` 参数,回显里看不到签名是正常的,不代表没签。坑 3:通知、导航语音一响,音乐就断了
v5.1.2 之前,只要其他 App 有短促声音(通知、导航语音、微信语音),正在播的音乐就被暂停。这其实是一个"太听话"的音频焦点问题。`audio_session` 拿到了焦点,但把任何焦点损失都当成了必须暂停。正确做法是按丢焦类型分级:电话来电、其他媒体 App 抢占 → 暂停(结束后自动续播);普通通知、导航、语音短促提示音 → 保持播放,只压低音量即可。调整后,除非真被电话打断,否则音乐不再被"一声通知"打断。坑 4:一个 48MB 的内嵌 Node.js,让 APK 体积冲到了 76MB
早期版本内嵌了一个 Node.js 18 运行时(`libnode.so`,约 48MB)来做本地代理。直接后果:APK 单架构就有 76MB。期间我们评估过纯 Dart 方案(零原生代码、跨平台、调试简单,理论上最优雅),但 AES 解密、签名校验这些细节验证成本高、坑多。最终正式版选择了 Rust 重写:`tiny_http` 做本地 HTTP 服务器,`ureq` 转发上游,覆盖 160+ 酷狗接口,交叉编译成 `libkugou_server.so`(arm64 / armv7 / x86 / x86_64 四个 ABI),通过 JNI/MethodChannel 启动。结果:APK 从 76MB 降到 27.7MB,启动更快、内存占用更低,还顺手解决了端口冲突(改用随机端口)和登录扫码校验失败(登录直连酷狗,不再依赖第三方转发)等问题写在最后
- 真机验证永远大于状态判断。"录屏就好了"这种怪现象,往往指向渲染/调度层而不是数据层;
- 外部接口的字段"存在"不代表"成功"。比如云盘上传接口成功响应里也会带
error_code: 0,用"字段是否存在"判断失败会把所有上传误判为被拒——这种 bug 往往过了好几天才暴露; - 升级大依赖前读 changelog,尤其是播放器这种底层库;
- 架构瘦身是性价比最高的优化:48MB 的运行时换成一个 1~2MB 的
.so,比优化几百行 Dart 代码强得多。
如果你也对"用 Flutter 搓一个正经音乐播放器"感兴趣,项目开源在 GitHub(仅供学习),欢迎交流。