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 知识体系总览

下面这张图帮你把今天的内容串起来。从取消的三种方式,到停止时的清理流程,一目了然。

任务取消与停止知识体系 cancelWorkById 取消单个任务 cancelAllWorkByTag 按标签批量取消 cancelUniqueWork 取消唯一任务 任务收到停止信号 主动检查 isStopped 在循环中快速退出 被动回调 onStopped 做一次性清理工作 任务安全结束

10.6 避坑总结

  • 取消不是立即生效:WorkManager 需要时间处理取消请求,不要假设调用后任务立刻消失。
  • onStopped 里别做重活:我曾经在里面写了个文件压缩,结果导致后续任务排队超时。轻量清理,快速返回。
  • 检查 isStopped 要放在循环里:如果 doWork 里只有一个大操作,没有循环,那 isStopped 基本没用。记得在关键步骤前后检查。
  • 唯一任务的取消要配合策略:KEEP 和 REPLACE 的行为差异很大,选错了可能取消不掉你想取消的任务。

好了,任务取消这块就聊这么多。说白了就是三句话:知道怎么取消单个,知道怎么批量取消,知道停止时怎么收拾残局。把这三点吃透,后台任务调度就算入门了。