如何在 WorkManager 中实现任务依赖(Chain)和取消特定任务?
解读
国内面试场景下,这道题常被用来区分“用过”与“真正落地”过 WorkManager 的候选人。
面试官想听的不只是 API 名字,而是:
- 任务链在业务中的典型形态:串行、并行、树状、交叉依赖;
- 取消粒度:按 id、按 tag、按链、按唯一链(UniqueWork);
- 取消后如何防止脏数据、如何感知状态、如何与前台 UI 联动;
- 对国产 ROM 后台限制的兜底策略(JobScheduler 被拦截、电量优化、AlarmManager fallback);
- 线上崩溃与 ANR 的排查经验(WorkManager 版本差异、Room 数据库锁、Worker 构造函数无参检查)。
回答时务必结合真实业务:例如“图片上传链”先压缩→加密→上传→上报,用户点击“取消上传”按钮后,整条链要干净地停掉并删除临时文件。
知识点
- WorkRequest 的三种类型:OneTimeWorkRequest、PeriodicWorkRequest、CoroutineWorker;
- 链式 API:beginWith() / then() / combine() / WorkContinuation;并行子链用 List<OneTimeWorkRequest> 一次性注入;
- 唯一链:enqueueUniqueWork(String uniqueName, ExistingWorkPolicy, WorkContinuation) 可防止重复入队;
- 取消 API:WorkManager.cancelWorkById(UUID)、cancelAllWorkByTag(String)、cancelUniqueWork(String)、cancelAllWork();
- 取消信号:Worker 内监听 isStopped() 或 Kotlin 的 coroutineContext.isActive,及时释放 native 资源、删除临时文件;
- 状态回调:WorkInfo.State.CANCELLED 会立即回调到 LiveData/Flow,UI 层可弹 Toast 并重置按钮;
- 数据库一致性:链中某个节点被取消,后续节点默认标记为 CANCELLED,不会执行;若需要“无论成败都清理”,可在 finally 块注册 cancel 监听器;
- 国产 ROM 适配:华为/小米/OPPO 对 JobScheduler 的 9 分钟调度限制,需在后台保活白名单引导页提示用户,或降级使用 setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST);
- 版本坑:2.7.0 之前 enqueueUniqueWork 的 ExistingWorkPolicy.REPLACE 不会自动取消旧链,导致重复上传;升级后需显式调用 cancelUniqueWork 再入队;
- 调试工具:adb shell dumpsys jobscheduler | grep your.package 查看 JobId;App 内集成 WorkManagerInspector 可实时查看链状态。
答案
以“图片上传”业务为例,演示“压缩→加密→上传→上报”链及用户随时取消。
- 定义 Worker
class CompressWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val uri = inputData.getString("img_uri") ?: return Result.failure()
while (isActive) { // 监听取消信号
// 模拟压缩
delay(500)
break
}
if (!isActive) { // 被用户取消
File(uri).delete() // 清理临时文件
return Result.failure()
}
val outUri = compress(uri)
return Result.success(workDataOf("compress_uri" to outUri))
}
}
class EncryptWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val uri = inputData.getString("compress_uri") ?: return Result.failure()
if (!isActive) return Result.failure()
val outUri = encrypt(uri)
return Result.success(workDataOf("encrypt_uri" to outUri))
}
}
class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val uri = inputData.getString("encrypt_uri") ?: return Result.failure()
if (!isActive) return Result.failure()
val url = upload(uri)
return Result.success(workDataOf("remote_url" to url))
}
}
class ReportWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val url = inputData.getString("remote_url") ?: return Result.failure()
if (!isActive) return Result.failure()
report(url)
return Result.success()
}
}
- 组装链并入队
val compress = OneTimeWorkRequestBuilder<CompressWorker>()
.addTag("upload_chain")
.setInputData(workDataOf("img_uri" to localUri))
.build()
val encrypt = OneTimeWorkRequestBuilder<EncryptWorker>()
.addTag("upload_chain")
.build()
val upload = OneTimeWorkRequestBuilder<UploadWorker>()
.addTag("upload_chain")
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
val report = OneTimeWorkRequestBuilder<ReportWorker>()
.addTag("upload_chain")
.build()
val chain = WorkManager.getInstance(context)
.beginUniqueWork(
"unique_upload_${localUri.hashCode()}",
ExistingWorkPolicy.REPLACE, // 防止重复点击生成多条链
compress
)
.then(encrypt)
.then(upload)
.then(report)
chain.enqueue()
- 取消指定链
fun cancelUpload(localUri: String) {
val uniqueName = "unique_upload_${localUri.hashCode()}"
WorkManager.getInstance(context).cancelUniqueWork(uniqueName)
}
- UI 监听
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkLiveData("unique_upload_${localUri.hashCode()}")
.observe(owner) { list ->
val finished = list?.any {
it.state == WorkInfo.State.CANCELLED ||
it.state == WorkInfo.State.FAILED ||
it.state == WorkInfo.State.SUCCEEDED
} == true
if (finished) {
// 刷新按钮状态
}
}
- 国产 ROM 兜底 在 Application 初始化时:
val config = Configuration.Builder()
.setJobSchedulerJobIdRange(1000, 2000)
.setMinJobSchedulerId(1000)
.build()
WorkManager.initialize(this, config)
并在设置页引导用户关闭“省电优化”,确保链节点能在 9 分钟内被调度。
拓展思考
-
如果链中某个节点失败,如何只重试该节点而不重跑整条链?
答:给单个 WorkRequest 设置setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS),并在 doWork() 返回Result.retry();其余节点无需改动,WorkManager 会自动重试失败节点,成功后继续下游。 -
如何支持“断点续传”式上传?
答:在 UploadWorker 的 inputData 里带上已上传字节数,上传前查询服务器已接收长度,使用HttpURLConnection.setRequestProperty("Range", "bytes=$alreadyUploaded-")继续传;若 Worker 被系统杀死,下次重试时仍能从断点继续。 -
多端登录场景下,如何防止另一台设备重复上传同一张图片?
答:利用唯一链名称里加入用户 ID + 图片 MD5,服务器上传接口先校验 MD5 是否已存在,若存在直接返回 URL,客户端 ReportWorker 依旧执行,保证两端数据一致。 -
如果业务要求“加密与上传并行”以缩短耗时,如何改造?
答:将加密和上传设为并行节点,使用WorkContinuation.combine()合并两条子链,最后统一接入 ReportWorker;注意加密输出 URI 要放入 Data,上传节点读取即可。 -
线上出现“链状态卡在 ENQUEUED 但一直不执行”如何排查?
答:先通过adb shell dumpsys jobscheduler | grep your.package看 JobId 是否被系统取消;再检查是否被省电策略拦截;最后确认 WorkManager 版本是否低于 2.7.0,低版本在 Android 12 上缺少 expedited 权限导致无法调度,升级后加setExpedited()即可。