根治BiliDownload下载条目重复问题:从源码分析到彻底解决
【免费下载链接】BiliDownload Android Bilibili视频下载器 项目地址: https://gitcode.com/gh_mirrors/bi/BiliDownload
你是否还在为BiliDownload中同一视频多次下载导致的存储浪费和管理混乱而困扰?本文将深入剖析下载条目重复问题的底层原因,提供三种递进式解决方案,并附赠完整实现代码,帮助开发者彻底解决这一痛点。读完本文你将获得:
- 精准识别重复下载的技术原理
- 三种解决方案的实现指南与性能对比
- 预防重复下载的架构设计最佳实践
问题现象与影响范围
BiliDownload作为Android平台的Bilibili视频下载工具,在用户高频使用场景下常出现下载条目重复创建的问题。典型表现为:
- 同一视频(相同BVID+CID)可被多次添加到下载队列
- 重复下载的视频片段存储在独立目录,造成存储空间浪费
- 下载历史列表充斥重复条目,影响用户体验
通过对GitHub加速计划仓库(https://gitcode.com/gh_mirrors/bi/BiliDownload)的源码分析,我们发现该问题主要源于下载任务创建流程中缺乏有效的重复校验机制。
技术原理与源码分析
下载任务创建流程
BiliDownload的下载管理核心位于DownloadManager.kt和DownloadRepository.kt,关键流程如下:
问题根源定位
在DownloadRepository.kt的createNewRecord函数中,我们发现创建下载记录时直接插入数据库,未进行重复性校验:
@Throws(IllegalStateException::class, SQLiteConstraintException::class)
suspend fun createNewRecord(
bvid: String,
cid: Long,
resources: List<BiliDashModel>
): Long {
val taskId = mDownloadTaskDao.insert(
DownloadTaskEntity.createEntity(bvid, cid)
).also { if (it == -1L) throw IllegalStateException("Invalid download task id") }
// 直接插入新记录,无重复检查
resources.map {
DownloadDashEntity(
dashId = it.dashId,
taskId = taskId,
codecId = it.codecId,
type = it.type,
codecs = it.codecs,
mimeType = it.mimeType
)
}.let { mDownloadDashDao.insertOrUpdate(*it.toTypedArray()) }
return taskId
}
同时在DownloadManager.kt的启动下载流程中,也未对即将创建的任务进行前置检查:
suspend fun startDownload(
context: Context,
bvid: String,
cid: Long,
resources: List<BiliDashModel>
) = DownloadRepository.createNewRecord(bvid, cid, resources).also {
DownloadService.startDownload(context, it)
}
这种"直接创建"的设计导致只要调用startDownload就会产生新的下载记录,完全没有考虑重复情况。
解决方案实现
针对以上问题,我们提供三种解决方案,可根据项目实际需求选择实施:
方案一:数据库唯一约束(快速修复)
实现原理:在数据库层面为DownloadTaskEntity表添加BVID+CID组合唯一约束,从存储层防止重复记录创建。
实施步骤:
- 修改数据库实体类
DownloadTaskEntity.kt,添加唯一约束注解:
@Entity(
tableName = "download_task",
uniqueConstraints = [UniqueConstraint(columnNames = ["bvid", "cid"])]
)
data class DownloadTaskEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val bvid: String,
val cid: Long,
// 其他字段...
)
- 在
DownloadRepository.kt的createNewRecord函数中捕获唯一约束异常:
@Throws(IllegalStateException::class, SQLiteConstraintException::class)
suspend fun createNewRecord(
bvid: String,
cid: Long,
resources: List<BiliDashModel>
): Long {
return try {
val taskId = mDownloadTaskDao.insert(
DownloadTaskEntity.createEntity(bvid, cid)
).also { if (it == -1L) throw IllegalStateException("Invalid download task id") }
// 插入Dash记录代码...
taskId
} catch (e: SQLiteConstraintException) {
// 查询已存在的任务ID
mDownloadTaskDao.getTaskIdByBvidAndCid(bvid, cid)
?: throw IllegalStateException("Duplicate task but not found existing record")
}
}
- 在
DownloadTaskDao.kt中添加查询方法:
@Query("SELECT id FROM download_task WHERE bvid = :bvid AND cid = :cid LIMIT 1")
suspend fun getTaskIdByBvidAndCid(bvid: String, cid: Long): Long?
优点:实现简单,数据库层保障数据一致性
缺点:依赖异常捕获机制,性能略差;无法区分不同状态的重复任务
方案二:业务层前置检查(推荐方案)
实现原理:在创建下载任务前主动检查数据库中是否存在相同BVID+CID的有效任务,避免重复创建。
// DownloadRepository.kt
suspend fun checkDuplicateTask(bvid: String, cid: Long): Boolean {
val existingTask = mDownloadTaskDao.getValidTaskByBvidAndCid(bvid, cid)
return existingTask != null
}
// DownloadTaskDao.kt
@Query("""
SELECT id FROM download_task
WHERE bvid = :bvid AND cid = :cid
AND status NOT IN (:invalidStatuses)
LIMIT 1
""")
suspend fun getValidTaskByBvidAndCid(
bvid: String,
cid: Long,
invalidStatuses: List<Int> = listOf(
TaskStatus.CANCELLED.code,
TaskStatus.FAILED.code
)
): Long?
修改DownloadManager.kt的启动下载流程:
suspend fun startDownload(
context: Context,
bvid: String,
cid: Long,
resources: List<BiliDashModel>
): Long {
// 检查是否存在有效下载任务
if (DownloadRepository.checkDuplicateTask(bvid, cid)) {
throw IllegalStateException("Duplicate download task detected")
}
return DownloadRepository.createNewRecord(bvid, cid, resources).also {
DownloadService.startDownload(context, it)
}
}
优点:主动检查机制,性能优于异常捕获;可自定义有效任务判定逻辑
缺点:需处理并发场景下的竞态条件
方案三:下载队列去重机制(彻底解决方案)
实现原理:结合内存缓存和数据库检查,实现下载队列的实时去重,完整实现如下:
// DownloadManager.kt
// 内存缓存:BVID+CID -> TaskId
private val mDownloadingTasks = ConcurrentHashMap<String, Long>()
suspend fun startDownload(
context: Context,
bvid: String,
cid: Long,
resources: List<BiliDashModel>
): Long {
val key = "$bvid:$cid"
// 1. 内存缓存检查
mDownloadingTasks[key]?.let {
// 返回已存在的任务ID
return it
}
// 2. 数据库检查
if (DownloadRepository.checkDuplicateTask(bvid, cid)) {
throw IllegalStateException("Duplicate download task detected")
}
// 3. 创建新任务并加入缓存
return try {
val taskId = DownloadRepository.createNewRecord(bvid, cid, resources)
mDownloadingTasks[key] = taskId
DownloadService.startDownload(context, taskId)
taskId
} catch (e: Exception) {
mDownloadingTasks.remove(key)
throw e
}
}
// 任务结束时清理缓存
private fun onTaskEnd(task: DownloadTaskEntity) {
val key = "${task.biliBvid}:${task.biliCid}"
mDownloadingTasks.remove(key)
// 其他清理逻辑...
}
缓存一致性保障:
- 任务正常结束时主动清理缓存
- 应用启动时从数据库加载活跃任务到缓存
- 定时任务定期同步数据库状态到缓存
优点:三层防护机制(内存+数据库+约束),彻底解决重复问题
缺点:实现复杂度较高,需处理缓存一致性
解决方案对比与选择建议
| 方案 | 实现难度 | 性能 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 数据库约束 | ★☆☆☆☆ | 中 | 高 | 快速修复,简单场景 |
| 业务层检查 | ★★☆☆☆ | 高 | 高 | 大多数应用场景 |
| 队列去重机制 | ★★★☆☆ | 最高 | 最高 | 高并发场景,追求极致体验 |
推荐选择策略:
- 个人开发者/小团队:优先采用方案二,兼顾实现复杂度和可靠性
- 追求极致体验:采用方案三,特别是在频繁下载场景下
- 紧急修复:先用方案一快速止血,后续迭代优化为方案二或三
完整实现代码
以下是方案二(业务层前置检查)的完整实现代码,适用于大多数应用场景:
// DownloadRepository.kt 新增方法
suspend fun checkDuplicateTask(bvid: String, cid: Long): Boolean {
val existingTask = mDownloadTaskDao.getValidTaskByBvidAndCid(bvid, cid)
return existingTask != null
}
// DownloadTaskDao.kt 新增方法
@Query("""
SELECT id FROM download_task
WHERE bvid = :bvid AND cid = :cid
AND status NOT IN (:cancelledStatus, :failedStatus)
LIMIT 1
""")
suspend fun getValidTaskByBvidAndCid(
bvid: String,
cid: Long,
cancelledStatus: Int = TaskStatus.CANCELLED.code,
failedStatus: Int = TaskStatus.FAILED.code
): Long?
// DownloadManager.kt 修改startDownload方法
suspend fun startDownload(
context: Context,
bvid: String,
cid: Long,
resources: List<BiliDashModel>
): Long {
// 检查是否存在有效下载任务
if (DownloadRepository.checkDuplicateTask(bvid, cid)) {
// 可选择返回已有任务ID或抛出异常
throw IllegalStateException("视频已在下载队列中或已完成下载")
}
return DownloadRepository.createNewRecord(bvid, cid, resources).also {
DownloadService.startDownload(context, it)
}
}
// VideoDetailsActivity.kt 添加用户提示
fun onDownloadClick() {
lifecycleScope.launch {
try {
val taskId = DownloadManager.startDownload(context, bvid, cid, selectedResources)
showToast("成功添加到下载队列")
} catch (e: IllegalStateException) {
showToast(e.message ?: "下载失败")
}
}
}
预防重复下载的架构设计最佳实践
为从根本上避免类似问题,建议采用以下架构设计原则:
1. 领域模型设计优化
引入DownloadTaskIdentifier值对象封装BVID+CID组合,统一重复校验逻辑:
data class DownloadTaskIdentifier(val bvid: String, val cid: Long) {
override fun toString(): String = "$bvid:$cid"
}
2. 仓储层接口设计
在仓储层定义明确的防重复接口:
interface IDownloadRepository {
// 返回结果封装:是否新建+任务ID
suspend fun createUniqueDownloadTask(
identifier: DownloadTaskIdentifier,
resources: List<BiliDashModel>
): Pair<Boolean, Long>
}
3. 业务规则引擎
将下载规则(如重复检查、空间检查等)抽象为独立引擎:
class DownloadBusinessEngine(
private val repository: IDownloadRepository,
private val spaceManager: SpaceManager
) {
suspend fun validateAndCreateTask(
identifier: DownloadTaskIdentifier,
resources: List<BiliDashModel>
): Result<Long> {
// 1. 检查空间是否充足
if (!spaceManager.hasEnoughSpace(resources)) {
return Result.failure(InsufficientSpaceException())
}
// 2. 检查是否重复
val (isNew, taskId) = repository.createUniqueDownloadTask(identifier, resources)
if (!isNew) {
return Result.failure(DuplicateTaskException(taskId))
}
return Result.success(taskId)
}
}
总结与展望
BiliDownload的下载条目重复问题,表面是简单的功能缺陷,实则反映了移动应用开发中数据一致性保障的普遍挑战。通过本文提供的三种解决方案,开发者可以根据项目实际情况选择合适的实现方式,彻底解决这一问题。
后续优化方向:
- 实现"继续下载"功能,允许用户恢复失败/暂停的任务
- 添加智能去重策略,如识别同一视频的不同清晰度版本
- 引入用户配置项,允许选择重复行为(忽略/提示/替换)
【免费下载链接】BiliDownload Android Bilibili视频下载器 项目地址: https://gitcode.com/gh_mirrors/bi/BiliDownload
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



