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 会把任务标记为失败,重试策略也就废了。
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 任务生命周期与调度冲突的核心流程:
你看这张图就清楚了:冲突的根源往往在于两个任务在“约束满足”阶段挤到了一起。比如两个不同的 Worker 都设置了网络约束和相同的延迟时间,当网络恢复瞬间,它们可能会被同时触发执行,争抢线程资源,导致帧率抖动。
5. 一个小案例:我在版本升级中踩的坑
有一次我负责做日志上传和配置拉取的模块。两个任务都用了 setInitialDelay(0) 和 NetworkType.CONNECTED。结果启动后,它们几乎同时执行,后台线程池瞬间被打满,主线程虽然没被阻塞,但 UI 线程因为频繁的系统回调(比如 ContentProvider 同步)开始掉帧。
这就是典型的 调度冲突引起的帧率抖动。解决办法其实很简单:给其中一个任务加几秒延迟,或者使用 beginUniqueWork 把它们串行化。
要点小结:
- 定义 Worker:继承
Worker,重写doWork(),注意异常处理。 - 创建 WorkRequest:用 Builder 配置约束、退避策略、延迟、Tag。
- 调度:
enqueue()只是提交,不保证立即执行。 - 观察结果:通过
LiveData或ListenableFuture监听状态变化。 - 冲突点:多个任务同时满足约束 → 抢占线程 → 帧率抖动。