重装后收藏/喜欢 歌曲不见了:一次「ID 稳定性 + 数据迁移」的修复记录

1171 字
6 分钟
重装后收藏/喜欢 歌曲不见了:一次「ID 稳定性 + 数据迁移」的修复记录

重装后收藏/喜欢不见了:一次「ID 稳定性 + 数据迁移」的修复记录#

最近有个很典型、也很容易被忽略的问题:应用重装之后,收藏的歌曲、喜欢的歌曲看起来“全没了”

更准确地说是:收藏数据其实还在本地存储里,但 UI 匹配不到对应的歌曲,于是喜欢列表/歌单展示为空。

现象与日志#

用户侧的表现很直观:

  • 重装 App
  • 重新扫描本地歌曲(或重新授权)
  • 「我喜欢的音乐」变成空
  • 收藏/喜欢按钮状态也全变回未收藏

日志非常“打脸”:一边说 favorites 确实加载出来了,另一边却说匹配结果为 0。

[Favorites] Loaded 17 favorites: {804604524, 749312547, ...}
[Match Debug] First 3 song IDs: [1414957450, 147442901, 1927087886]
[Match Debug] First 3 favorites: [804604524, 749312547, 37290334]
[Match Debug] Matched 0 songs out of 17 favorites

这说明两件事:

  1. favorites 的持久化没坏(能读到 17 个 id)
  2. songs 列表也没坏(扫描出了歌曲 id)
  3. favorites 里存的 id,和当前扫描出来的 song.id 已经不是同一套体系

排查路径:到底是谁变了?#

收藏/喜欢的存储很简单:本地只存 Set<int> 的 songId(shared_preferences),UI 展示时用:

final favoriteSongs = songs.where((s) => favorites.contains(s.id)).toList();

因此只要 s.id 的生成规则发生变化(或同一首歌在重装后拿到的 id 变了),favorites 就会“全部失效”。

我把排查重点放到两类常见不稳定来源:

1)系统媒体库 id 不稳定#

Android/iOS 的系统媒体库可能返回一个“看起来像主键”的 id(例如 on_audio_querySongModel.id),但它未必承诺跨重装、跨版本、跨扫描一致。

2)用「文件绝对路径」生成 id,会被 iOS 重装打爆#

iOS 卸载重装后,App 的沙盒容器路径会变(例如 .../Application/<UUID>/Documents/... 里的 <UUID> 变了)。

如果 id 是 hash(绝对路径),那同一个文件在新容器里就会产生完全不同的 id。

更糟的是:我之前为了修别的问题,把 id 从一种 hash 改成了另一种(比如 String.hashCode vs 自定义 hash),这也会让旧数据瞬间“断链”。

定位到关键点:收藏/歌单依赖「稳定 songId」#

播放器里有三处会依赖 songId:

  • 喜欢/收藏(favorite_songs
  • 本地歌单(歌单里存 songIds)
  • 播放状态恢复(缓存 playlist songIds + index)

只要 songId 不稳定,这三个功能都会在“升级/重装/换机”时出现类似问题。

所以修复目标不是“让匹配代码更聪明”,而是:

  1. 定义一套跨重装稳定的 songId 规则
  2. 对历史版本产生的旧 id 做迁移

解决方案一:canonical path + 稳定 hash#

我新增了一个 SongId 工具(lib/core/services/song_id.dart),做两件事:

  1. 对文件路径做 canonicalize:
    • 如果文件位于 app 的 Documents 下,把“安装相关”的前缀剥离掉
    • 把路径变成 "<DOCS>/Music/xxx.flac" 这种相对且稳定的形式
  2. 对 canonical path 做确定性 hash(djb2,并限定 31-bit 正整数)

核心思路:同一首歌只要相对路径不变,重装后 id 也不变

解决方案二:启动时自动迁移旧数据#

光有新规则还不够:用户重装/升级后,preferences 里存的还是旧 id。

因此我在“扫描歌曲完成后”(LocalSongsNotifier.scanSongs())追加了迁移步骤(lib/core/services/providers.dart):

  1. 用当前扫描到的 songs 建一张映射表:
    • key:旧算法可能生成的 legacyId(例如 filePath.hashCode、旧的 hash)
    • value:当前 canonicalId(新规则算出来的 song.id
  2. 用这张表批量改写:
    • favorites(FavoritesService.migrateSongIds
    • playlists(PlaylistService.migrateSongIds,并保持原顺序去重)

迁移策略里有个小细节:如果同一个 legacyId 映射到多个 canonicalId(冲突),我会丢弃这个 legacyId 的映射,避免误把 A 歌迁到 B 歌。

验证与结果#

我补了一个最小单测,专门验证“iOS 容器路径变了,id 仍然一致”(test/song_id_test.dart):

  • .../Application/AAA/Documents/Music/foo.mp3
  • .../Application/BBB/Documents/Music/foo.mp3

canonicalize 后都应变成 "<DOCS>/Music/foo.mp3",因此 id 相同。

最终效果:

  • 重装/升级后,favorites 仍然能匹配到歌曲
  • 喜欢列表不再空
  • 本地歌单不会因为 id 体系变化而“全失效”

小结#

这类问题的根本原因通常不是“收藏没存上”,而是 你存的是一个不稳定的引用(songId)

经验总结:

  • 只要你把业务数据(收藏/歌单/播放缓存)建立在某个 id 上,就要把这个 id 当作“长期协议”来设计
  • iOS 沙盒路径在重装后会变,绝对路径直接参与 id 计算会天然不稳定
  • 一旦你不得不改 id 规则,务必提供迁移,否则用户数据会在升级后“看起来丢了”

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
重装后收藏/喜欢 歌曲不见了:一次「ID 稳定性 + 数据迁移」的修复记录
https://www.ymxx.net/posts/favorites-reinstall-fix/
作者
Leguan
发布于
2026-01-16
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
在线播放切歌“声音和信息不一致”:一次从入口补丁到状态链路重构的排查
开发日志记录一次 Flutter 音乐播放器在线切歌错位问题:音频已切到下一首,但封面/歌名滞后,甚至要点暂停才刷新。包含踩坑方案、失败原因和最终稳定修复。
2
Android/Flyme 通知栏“暂停后继续播放无响应”技术复盘:根因、关键代码与最终稳定方案
开发日志一次真实线上故障的技术复盘:Flyme 机型通知栏暂停后无法继续播放。包含时序根因、关键代码改动、日志证据、兼容策略与验证结果。
3
Flutter/iOS 后台自动切歌与通知栏手动“下一首”稳定实现方法
开发日志Flutter/iOS 后台自动切歌与通知栏“下一首”稳定实现方法前段时间在重构播放器内核时,我顺手把一类最烦的 iOS 播放问题彻底收了一遍:后台自动切歌不稳定、锁屏/通知栏点“下一首”偶发失...
4
播放页默认封面切换闪烁(技术细节版):从资源猜测到动画结构稳定性的完整修复
开发日志技术细节版复盘:记录播放器在“无封面歌曲”场景下切换歌词页时闪烁的问题,包含日志观察、错误方案、最终代码改动与可复用排查模板。
5
iOS 本地 FLAC 拖动进度条不准:一次「日志正确但耳朵不对」的排查
开发日志最近在重写一个音乐播放器软件,拖动进度条时歌曲的实际播放进度和进度条显示的不一致,记录一下 Bug 的修复过程。
随机文章随机推荐

评论区

Profile Image of the Author
Leguan
Hello, I'm Leguan.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
15
说说
21
分类
3
标签
23
总字数
23,182
运行时长
0
最后活动
0 天前
站点信息
构建平台
Unknown CI
博客版本
Firefly v6.13.3
文章许可
CC BY-NC-SA 4.0

文章目录