流声里有个 bug:开着「自动下载最新一集」,某天又手动点了同一集的下载,结果那集音频是坏的,点取消也没反应。
这个 bug 值得写,它是三个小问题叠在一起,漏掉哪个都修不干净。
两条路径撞在一起
开了「自动下载最新一集」的节目,刷新缓存时会自动把最新一集存到本机。这时候我正好手点同一集,两条下载路径就同时起来了。
两条路径各自先判断一下「这集是不是在下载」。问题是这个判断和真正的下载之间,隔了两个 await——一个查 WiFi 设置,一个读存储。await 会让出事件循环,另一条路径就在这个空隙里挤进来。
于是两边都看到「没在下载」,都往下走。
两个字节流写同一个文件
下载用 Dio,先写到一个临时文件 .part,下完再改名。这个临时文件的名字只由 guid 决定,所以两条路径打开的是同一个文件。
两个 Dio 各自按自己的节奏收数据、往里写,字节流交错落地。下出来的文件是坏的。
取消为什么没反应
cancel token 原本按 guid 存一个。第二条路径启动时,把先启动那次的 token 顶掉了。
顶掉之后,先启动的那次再也取消不掉——点取消,取消的是后启动的那个。用户看到的就是「按了没反应」。
三处修法
临时文件加唯一后缀。 时间戳加序号,两次并发下载各写各的,不再撞车。
cancel token 按 guid 存集合。 一个 guid 可能挂着好几个 token,取消时全停掉。清理时只摘掉自己那个,不能整条 remove,否则又把并发的那条抹了。
判重和占位不能跨 await。 这才是根子:
// 判重和占位之间不能跨 awaitif (_inflightFeedByGuid.containsKey(episode.guid)) return;_inflightFeedByGuid[episode.guid] = feed.id; // 先占位final wifiOnly = await ...; // 再 await把「占住这一集」前移到第一个 await 之前,另一条路径进来时看到的就已经是「在下载」。
占位还有个细节:如果 WiFi 检查不通过要还回去,不然这一集永远下不了。
之后
现在同一集的并发下载各写各的临时文件,取消时一起停。损坏音频和「取消没反应」都没再出现。
这种事以后大概还会犯。教训就一条:判重和占位之间,别跨 await。