WorkManager 基本用法:定义 Worker,创建 WorkRequest,调度任务

说实话,每次接手新项目看到后台任务的管理,我第一反应就是——千万别自己写线程池。原因很简单:你永远不知道系统会在什么时候杀掉你的进程。嗯,WorkManager 就是来帮你解决这个问题的。

我们先搞清楚它的核心三步:定义任务逻辑 → 配置执行规则 → 提交给系统。说白了,就是定义一个 Worker,创建一个 WorkRequest,然后 enqueue 出去。下面我一个个说。

1. 定义 Worker:你的任务逻辑长什么样

Worker 是 WorkManager 的执行单元。你只需要继承 Worker 类,在 doWork() 里写你的业务逻辑。我建议把耗时操作放在这里,但注意它默认运行在后台线程。

public class UploadWorker extends Worker {
    public UploadWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public Result doWork() {
        // 这里写你的耗时代码,比如上传日志
        try {
            // 模拟网络上传
            Thread.sleep(2000);
            return Result.success();
        } catch (Exception e) {
            return Result.failure();
        }
    }
}

我曾经在一个项目里把网络请求直接写在 doWork() 里,结果崩溃率直接上升。为什么?因为 doWork() 默认运行在 WorkManager 内部的后台线程池,你不需要再开子线程。但你一定要捕获异常,否则一旦抛出未捕获异常,WorkManager 会把任务标记为失败,重试策略也就废了。

⚠️ 注意:doWork() 最多执行 10 分钟。如果你有更长时间的任务(比如文件下载),用 ListenableWorker 或 CoroutineWorker 更合适。

2. 创建 WorkRequest:配置执行规则

Worker 定义了做什么,WorkRequest 则定义什么时候做、有什么约束。常见的有两种:OneTimeWorkRequest(一次性)和 PeriodicWorkRequest(周期性)。这里我重点说 OneTimeWorkRequest,因为它在冲突分析里最常被误用。

OneTimeWorkRequest uploadRequest = new OneTimeWorkRequest.Builder(UploadWorker.class)
        .setConstraints(new Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED) // 需要网络
                .build())
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
        .setInitialDelay(5, TimeUnit.SECONDS)
        .addTag("upload")
        .build();

这里每个细节都很重要。我记得有一次线上反馈说“任务不执行”,最后定位到是因为 Constraints 里同时要求了 NetworkType.CONNECTED 和 BatteryNotLow,而用户手机在省电模式下网络被切断了——任务永远不满足约束。说白了,约束越严,调度窗口越小。

配置项 作用 我的避坑经验
setConstraints() 指定执行条件(网络、充电、空闲等) 不要加太多约束,否则任务可能被无限推迟
setBackoffCriteria() 失败后重试策略(线性或指数退避) 指数退避更符合系统资源分配,我一般都选它
setInitialDelay() 延迟多久后首次执行 如果是周期任务,这个延迟非常关键,容易造成调度重叠
addTag() 给任务打标签,便于批量取消或查询 我习惯用业务模块名做 tag,排查问题快很多

3. 调度任务:把 WorkRequest 提交给系统

最后一步很简单,获取 WorkManager 实例,调用 enqueue() 即可。

WorkManager.getInstance(context).enqueue(uploadRequest);

这么做之后,系统会综合考虑电量、网络、空闲状态等因素来安排执行时间。但注意:enqueue 不会立即执行。我见过不少新人以为 enqueue 就是立即启动,结果下一个界面就等着任务结果——这是坑。

💡 如果你需要观察任务结果,可以用 LiveData 或 ListenableFuture。比如:

WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(uploadRequest.getId())
    .observe(lifecycleOwner, workInfo -> {
        if (workInfo.getState() == State.SUCCEEDED) {
            // 处理成功
        }
    });

4. 代码之外:你真的理解调度时序吗?

好,现在你已经会写基本的 Worker 了。但问题是:当你同时调度多个任务,或者前后任务有依赖关系时,系统是怎么处理的?

来看这张图,这是我梳理的 WorkManager 任务生命周期与调度冲突的核心流程:

WorkManager 任务调度与冲突关键流程 App 提交 WorkRequest 内部队列 + 约束检查 约束满足,进入调度 约束不满足,挂起等待 执行 doWork() Result.success() Result.failure() 失败后根据 BackoffCriteria 重试 约束变化时重新评估 注意调度时机冲突

你看这张图就清楚了:冲突的根源往往在于两个任务在“约束满足”阶段挤到了一起。比如两个不同的 Worker 都设置了网络约束和相同的延迟时间,当网络恢复瞬间,它们可能会被同时触发执行,争抢线程资源,导致帧率抖动。

5. 一个小案例:我在版本升级中踩的坑

有一次我负责做日志上传和配置拉取的模块。两个任务都用了 setInitialDelay(0) 和 NetworkType.CONNECTED。结果启动后,它们几乎同时执行,后台线程池瞬间被打满,主线程虽然没被阻塞,但 UI 线程因为频繁的系统回调(比如 ContentProvider 同步)开始掉帧。

这就是典型的 调度冲突引起的帧率抖动。解决办法其实很简单:给其中一个任务加几秒延迟,或者使用 beginUniqueWork 把它们串行化。

要点小结:

  • 定义 Worker:继承 Worker,重写 doWork(),注意异常处理。
  • 创建 WorkRequest:用 Builder 配置约束、退避策略、延迟、Tag。
  • 调度:enqueue() 只是提交,不保证立即执行。
  • 观察结果:通过 LiveData 或 ListenableFuture 监听状态变化。
  • 冲突点:多个任务同时满足约束 → 抢占线程 → 帧率抖动。