10、任务取消与停止:取消单个任务、取消所有带标签的任务、取消唯一任务、处理任务停止时的清理工作
后台任务调度,说白了就是让 App 在合适的时间干合适的活。但干活容易,收工难。你想想看,用户划掉任务列表,或者业务逻辑变了,正在跑的任务必须停下来。这时候怎么优雅地「喊停」,就是今天要聊的核心。
我个人习惯把任务取消分成三个维度:单个任务、按标签批量取消、唯一任务取消。再加上一个容易被忽略的——任务被停止时,你得把屁股擦干净。
核心要点:取消不等于立即杀死线程。WorkManager 会给任务一个「收到停止信号」的机会,让你做清理工作。这一点和 Thread.interrupt() 的设计思路很像。
10.1 取消单个任务:cancelWorkById
这是最基础的操作。每个 WorkRequest 都有一个 UUID,你拿着这个 ID 就能精准取消。
// 启动任务时保存 ID
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
.build()
val workId = workRequest.id // UUID
WorkManager.getInstance(context).enqueue(workRequest)
// 需要取消时
WorkManager.getInstance(context).cancelWorkById(workId)
我在项目中遇到过一个问题:用户连续点击「取消」按钮,导致 cancelWorkById 被调用了多次。其实这没关系,WorkManager 内部做了幂等处理,重复取消不会报错。但如果你在回调里做了 UI 更新,记得加个防抖。
小技巧:如果你需要知道取消是否成功,可以监听 LiveData<WorkInfo>。当状态变成 CANCELLED 时,说明任务确实被取消了。
10.2 取消所有带标签的任务:cancelAllWorkByTag
标签是个好东西。你可以给一组相关的任务打上同一个标签,然后一键取消。比如用户退出登录时,所有上传任务都应该停掉。
// 打标签
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
.addTag("upload")
.build()
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag("upload")
.build()
WorkManager.getInstance(context).enqueue(listOf(uploadWork, syncWork))
// 一键取消所有 upload 标签的任务
WorkManager.getInstance(context).cancelAllWorkByTag("upload")
嗯,这里要注意:cancelAllWorkByTag 取消的是所有未完成且带有该标签的任务。已经执行完的它不管。我曾经在调试日志里看到有人误以为它能取消历史任务,结果排查了半天。
避坑指南:我曾经在多个 WorkManager 实例上调用 cancelAllWorkByTag,发现只有当前进程的任务被取消了。如果你用了 RemoteWorkManager(跨进程场景),记得在对应的进程中调用取消方法。
10.3 取消唯一任务:cancelUniqueWork
唯一任务(UniqueWork)的设计初衷是防止重复。比如你只允许一个「同步通讯录」的任务在跑。取消它的时候,用 cancelUniqueWork 最合适。
// 定义唯一任务
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(
"sync_contacts", // 唯一名称
ExistingWorkPolicy.REPLACE, // 如果已有任务,替换掉
syncWork
)
// 取消唯一任务
WorkManager.getInstance(context).cancelUniqueWork("sync_contacts")
这里有个细节:ExistingWorkPolicy 会影响取消行为。如果你用的是 KEEP 策略,cancelUniqueWork 只会取消当前这个任务,之前排队等待的那个不会被取消。说白了,你得搞清楚「唯一」指的是哪个。
| 策略 | 取消行为说明 |
|---|---|
| REPLACE | 取消旧任务,执行新任务。cancelUniqueWork 取消的是当前正在运行的那个 |
| KEEP | 保留旧任务,忽略新任务。cancelUniqueWork 只取消新提交的(如果还没执行) |
| APPEND | 追加到队列末尾。cancelUniqueWork 会取消整个队列 |
| APPEND_OR_REPLACE | 追加或替换。取消行为同 APPEND |
10.4 处理任务停止时的清理工作
这才是今天的重头戏。任务被取消时,Worker 不会立刻消失。它会收到一个 onStopped() 回调,你可以在里面做清理。
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
// 模拟上传
for (i in 0 until 100) {
if (isStopped) {
// 收到停止信号,主动退出
cleanup()
return Result.failure()
}
// 执行上传逻辑
Thread.sleep(100)
}
return Result.success()
}
override fun onStopped() {
// 清理资源:关闭文件流、释放锁、取消网络请求
super.onStopped()
cleanup()
}
private fun cleanup() {
// 关闭数据库连接、删除临时文件等
Log.d("UploadWorker", "清理完成")
}
}
为什么既要检查 isStopped 又要重写 onStopped?我解释一下:isStopped 是主动检查,适合在循环中快速退出;onStopped 是被动回调,适合做一次性清理。两者配合,才能保证万无一失。
重要提醒:千万不要在 onStopped 里做耗时操作。WorkManager 的线程池是有限的,你占着茅坑不拉屎,其他任务就得等着。如果清理工作很重,考虑用协程或者另开线程。
我记得有一次线上事故:某个 Worker 在 onStopped 里执行了数据库的 VACUUM 操作,结果导致 UI 线程卡顿了好几秒。后来改成在 onStopped 里只标记状态,真正的清理交给一个轻量级的协程去做,问题就解决了。
10.5 知识体系总览
下面这张图帮你把今天的内容串起来。从取消的三种方式,到停止时的清理流程,一目了然。
10.6 避坑总结
- 取消不是立即生效:WorkManager 需要时间处理取消请求,不要假设调用后任务立刻消失。
- onStopped 里别做重活:我曾经在里面写了个文件压缩,结果导致后续任务排队超时。轻量清理,快速返回。
- 检查 isStopped 要放在循环里:如果 doWork 里只有一个大操作,没有循环,那 isStopped 基本没用。记得在关键步骤前后检查。
- 唯一任务的取消要配合策略:KEEP 和 REPLACE 的行为差异很大,选错了可能取消不掉你想取消的任务。
好了,任务取消这块就聊这么多。说白了就是三句话:知道怎么取消单个,知道怎么批量取消,知道停止时怎么收拾残局。把这三点吃透,后台任务调度就算入门了。