唯一工作序列:beginUniqueWork() 与 ExistingWorkPolicy

各位同学,今天我们来聊一个非常实用的知识点——唯一工作序列。说白了,就是如何保证你的后台任务不会重复执行。

我在做项目时遇到过这么个场景:用户点击同步按钮,结果网络卡了一下,他连点了三下。好家伙,三个同步任务同时跑起来了,数据乱成一锅粥。嗯,从那以后,我就对唯一工作序列格外上心。

什么是唯一工作序列?

唯一工作序列,就是给一组任务贴上一个独一无二的标签。WorkManager 通过这个标签来识别:这个任务是不是已经在排队了?是不是正在执行?我该怎么处理新的请求?

你想想看,这就像你去银行办业务。你拿了一个号,如果再去取号机按一下,系统会怎么处理?是给你一个新号,还是告诉你「你已经在排队了」?这就是 ExistingWorkPolicy 要解决的问题。

核心概念:beginUniqueWork() 方法允许你创建一个「有身份」的工作链。这个身份就是 workName,在整个应用中必须是唯一的。

beginUniqueWork() 的用法

先看代码,我习惯这样写:

val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(syncConstraints)
    .build()

WorkManager.getInstance(context)
    .beginUniqueWork(
        "sync_data",
        ExistingWorkPolicy.KEEP,
        syncWork
    )
    .enqueue()

这里的关键参数就两个:workName 和 ExistingWorkPolicy。workName 就是你的任务身份证,ExistingWorkPolicy 决定了「撞车」时怎么办。

ExistingWorkPolicy 三种策略详解

策略 行为 适用场景
KEEP 如果已有同名任务在排队或执行中,新任务被丢弃 同步任务防重复、数据上报
REPLACE 取消已有任务,用新任务替代 配置更新、用户信息刷新
APPEND 将新任务追加到已有任务链的末尾 链式操作、批量处理

KEEP 策略:我最常用的方案

说实话,KEEP 是我个人最常用的策略。为什么?因为大多数场景下,我们就是不想让任务重复。

举个例子,用户上传日志文件。用户可能频繁触发上传操作,但我们只需要一次上传就够了。用 KEEP 策略,后面的请求直接忽略,干净利落。

val uploadWork = OneTimeWorkRequestBuilder<LogUploadWorker>()
    .build()

WorkManager.getInstance(context)
    .beginUniqueWork(
        "upload_logs",
        ExistingWorkPolicy.KEEP,
        uploadWork
    )
    .enqueue()

小提示:KEEP 策略不会检查任务是否已经执行完毕。如果任务已经完成,新的请求会正常执行。它只关心「正在排队或执行中」的任务。

REPLACE 策略:当旧数据不再重要

REPLACE 策略就有点「喜新厌旧」的意思了。我记得有一次做用户配置同步功能,用户修改了设置,旧的同步任务还没跑完。这时候如果还用 KEEP,用户会看到旧数据。用 REPLACE 就对了——取消旧的,执行新的。

val configSync = OneTimeWorkRequestBuilder<ConfigSyncWorker>()
    .build()

WorkManager.getInstance(context)
    .beginUniqueWork(
        "sync_config",
        ExistingWorkPolicy.REPLACE,
        configSync
    )
    .enqueue()

注意:REPLACE 会调用 Worker.onStopped() 方法。如果你的 Worker 正在执行重要操作,记得在 onStopped() 中做好清理工作,避免数据损坏。

APPEND 策略:链式任务的粘合剂

APPEND 策略比较特殊。它不会取消旧任务,而是把新任务接到旧任务的后面。说白了,就是排队。

我一般在做批量图片上传时用这个策略。用户选了 10 张图,每张图是一个独立的上传任务。用 APPEND 策略,它们会按顺序执行,不会打架。

val firstUpload = OneTimeWorkRequestBuilder<ImageUploadWorker>()
    .setInputData(workDataOf("image_id" to 1))
    .build()

val secondUpload = OneTimeWorkRequestBuilder<ImageUploadWorker>()
    .setInputData(workDataOf("image_id" to 2))
    .build()

WorkManager.getInstance(context)
    .beginUniqueWork(
        "batch_upload",
        ExistingWorkPolicy.APPEND,
        firstUpload
    )
    .then(secondUpload)
    .enqueue()

实际应用场景:同步任务防重复

好了,理论知识讲完了。咱们来看看实际项目中怎么用。

我曾经做过一个电商 App,用户每次打开应用都要同步购物车数据。但问题是,用户可能频繁切换页面,每次切换都会触发同步。如果不做防重复,服务器会被打爆。

解决方案很简单:

  1. 在 Application 的 onCreate 中启动一个唯一工作序列
  2. 使用 KEEP 策略
  3. 设置合理的约束条件(网络可用、电量充足)
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        val syncConstraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
            
        val cartSync = OneTimeWorkRequestBuilder<CartSyncWorker>()
            .setConstraints(syncConstraints)
            .build()
            
        WorkManager.getInstance(this)
            .beginUniqueWork(
                "cart_sync",
                ExistingWorkPolicy.KEEP,
                cartSync
            )
            .enqueue()
    }
}

这样写的好处是:不管用户触发多少次同步请求,最终只会有一个任务在执行。既节省了用户流量,也减轻了服务器压力。

知识体系图

下面这张图帮你理清唯一工作序列的核心逻辑:

唯一工作序列核心逻辑 beginUniqueWork() 检查 workName 是否已存在 (任务身份证) 存在 不存在 ExistingWorkPolicy KEEP REPLACE APPEND 丢弃新任务 取消旧任务 追加到末尾 直接创建新任务 任务入队执行

避坑指南

最后,分享几个我踩过的坑:

  • 不要用 PeriodicWorkRequest 做唯一工作序列——它本身就有重复执行的特性,和唯一工作序列的理念冲突。我曾经这么干过,结果任务调度乱成一团。
  • 注意 workName 的命名规范——我建议用「模块名_功能名」的格式,比如 "sync_cart"、"upload_logs"。这样方便排查问题。
  • KEEP 策略不是万能的——如果你的任务需要更新数据,用 KEEP 可能导致用户看到旧数据。这时候 REPLACE 更合适。
  • APPEND 策略要小心链式任务——如果链式任务中有某个环节失败,整个链都会受影响。建议在 Worker 中做好错误处理。

我的习惯:在调试阶段,我会先用 REPLACE 策略,方便反复测试。上线前再根据实际需求切换到 KEEP 或 APPEND。

好了,唯一工作序列的内容就讲到这里。记住一句话:给任务一个身份,让重复不再发生。这是 WorkManager 中最实用的功能之一,也是保证后台任务稳定运行的关键。

公众号:蓝海资料掘金营,微信deep3321