重装后收藏/喜欢不见了:一次「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
这说明两件事:
- favorites 的持久化没坏(能读到 17 个 id)
- songs 列表也没坏(扫描出了歌曲 id)
- 但 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_query 的 SongModel.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 不稳定,这三个功能都会在“升级/重装/换机”时出现类似问题。
所以修复目标不是“让匹配代码更聪明”,而是:
- 定义一套跨重装稳定的 songId 规则
- 对历史版本产生的旧 id 做迁移
解决方案一:canonical path + 稳定 hash
我新增了一个 SongId 工具(lib/core/services/song_id.dart),做两件事:
- 对文件路径做 canonicalize:
- 如果文件位于 app 的
Documents下,把“安装相关”的前缀剥离掉 - 把路径变成
"<DOCS>/Music/xxx.flac"这种相对且稳定的形式
- 如果文件位于 app 的
- 对 canonical path 做确定性 hash(djb2,并限定 31-bit 正整数)
核心思路:同一首歌只要相对路径不变,重装后 id 也不变。
解决方案二:启动时自动迁移旧数据
光有新规则还不够:用户重装/升级后,preferences 里存的还是旧 id。
因此我在“扫描歌曲完成后”(LocalSongsNotifier.scanSongs())追加了迁移步骤(lib/core/services/providers.dart):
- 用当前扫描到的
songs建一张映射表:- key:旧算法可能生成的 legacyId(例如
filePath.hashCode、旧的 hash) - value:当前 canonicalId(新规则算出来的
song.id)
- key:旧算法可能生成的 legacyId(例如
- 用这张表批量改写:
- favorites(
FavoritesService.migrateSongIds) - playlists(
PlaylistService.migrateSongIds,并保持原顺序去重)
- favorites(
迁移策略里有个小细节:如果同一个 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 规则,务必提供迁移,否则用户数据会在升级后“看起来丢了”