目录
一、概述
二、核心三大件
2.1 定义 Worker 类
2.1.1 数据的传递
2.1.2 任务的结果
2.1.3 停止与取消
2.2 创建 WorkRequest
2.2.1 任务的类型:一次性 vs. 周期性
2.2.2 调度配置
2.2.3 约束条件
2.2.4 输入数据
2.2.5 添加优先级
2.3 提交任务 WorkManager
2.3.1 提交唯一任务
2.3.2 获取任务状态
(1) 任务状态 (WorkInfo)
(2) 通过 ID 获取(最精确)
(3) 通过 Unique Name 获取(最常用)
(4) 通过 Tag 观察任务状态
2.3.3 取消任务
三、任务链和复杂工作流
3.1 串行执行
3.2 并行执行
3.3 组合任务
系列入口导航:Android Jetpack 概述
一、概述
AndroidWorkManager是 Android Jetpack 组件库的一部分,专门用于处理持久性工作。所谓持久性工作,是指即便应用退出或设备重启,任务也必须执行的情况(例如向服务器同步数据或处理本地数据库)。
它会根据设备的 API 级别和应用状态,自动选择最合适的底层调度方式(如 JobScheduler、Custom AlarmManager 或 BroadcastReceiver)。
添加依赖:
dependencies { implementation 'androidx.work:work-runtime:2.9.0' }二、核心三大件
WorkManager 的基本构成:Worker(定义任务内容)、WorkRequest(定义任务如何运行)和WorkManager(正式调度任务)。
Worker (工人):这是你编写逻辑的地方。你继承 Worker 类并实现doWork()方法,告诉系统“要做什么”。
WorkRequest (任务请求):这是你的“订单”。你在这里规定任务是一次性的还是周期性的,以及具体的运行规则。
WorkManager (管理者):这是系统的指挥官。你把请求交给它,它负责在合适的时机调度执行。
2.1 定义 Worker 类
import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; public class MyUploadWorker extends Worker { private static final String TAG = "MyUploadWorker"; public MyUploadWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } @NonNull @Override public Result doWork() { // 执行实际任务(在后台线程运行) try { // 模拟耗时操作 uploadLogs(); return Result.success(); // 任务成功 } catch (Exception e) { Log.e(TAG, "任务失败", e); return Result.failure(); // 任务失败 } // 注意:不推荐使用 Result.retry(),因为会一直重试 } private void uploadLogs() { // 实际的上传逻辑 Log.d(TAG, "开始上传日志..."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } Log.d(TAG, "上传完成"); } }关键点: doWork() 方法运行在后台线程中,所以你可以在这里放心执行耗时操作(比如网络请求或数据库写入),而不会卡死界面。
2.1.1 数据的传递
Worker 并不是孤立运行的。你通常需要给它传参数(比如图片的 URL),同时也可以让它返回结果(比如处理后的文件路径)。有些任务只需要输入,有些只需要输出,而有些则两者兼有。
- 输入数据:在WorkRequest中通过setInputData(Data)设置。这个再后面详细说
- 获取数据:在Worker内部使用getInputData()。
- 输出数据:在返回Result.success(outputData)时携带数据。跟Result.success()不一样
注意:Data对象的大小限制为10KB。它适合传递路径、ID 或简单的标记,不适合传递大型位图或数据库条目。
2.1.2 任务的结果
doWork()必须返回一个 Result,这决定了 WorkManager 下一步怎么做:
| 结果类型 | 含义 |
Result.success() | 任务成功完成。 |
Result.failure() | 任务失败,不再重试。 |
Result.retry() | 任务失败,但告诉系统根据“退避政策”(Backoff Policy)稍后重试。 |
2.1.3 停止与取消
如果用户手动取消了任务,或者约束条件不再满足(比如 Wi-Fi 断了),系统会停止你的 Worker。
- 你可以通过isStopped()检查任务是否已被停止。
- 如果你的任务中有循环(比如上传大文件),你应该在循环中不断检查这个标志位,以便优雅地退出。
2.2 创建 WorkRequest
// 一次性任务 OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(MyUploadWorker.class) .build(); // 周期性任务(最小间隔15分钟) PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(MyUploadWorker.class, 15, TimeUnit.MINUTES) .build();2.2.1 任务的类型:一次性 vs. 周期性
根据执行频率,WorkRequest分为两个子类:
OneTimeWorkRequest:用于非重复性工作。
虽然是“一次性”,但如果设置了重试政策,它也可能多次运行。
PeriodicWorkRequest:用于定期运行的任务(如每晚备份数据)。
限制条件:最小间隔时间为15 分钟。
灵活时段 (Flex Interval):你可以指定任务在间隔期内的最后一段时间内运行。
2.2.2 调度配置
在构建请求时,你可以精确控制它的行为:
初始延迟 (Initial Delay): 使用 .setInitialDelay(10, TimeUnit.MINUTES)。即便约束条件已满足,任务也会等待指定时间后再执行。
OneTimeWorkRequest delayedWorkRequest = new OneTimeWorkRequest.Builder(MyUploadWorker.class) .setInitialDelay(5, TimeUnit.MINUTES) // 延迟5分钟执行 .build();指数退避策略 (Backoff Policy): 当Worker 返回 Result.retry()时,系统需要知道多久后重试。
Linear:每次重试间隔线性增加。
Exponential:每次重试间隔指数级增加(例如 10s, 20s, 40s...)。
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyUploadWorker.class) .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, // 退避策略:指数或线性 10, TimeUnit.SECONDS // 初始延迟时间 ) .build();标记 (Tagging): 通过 .addTag("cleanup") 给任务打标签。这非常有用,因为你可以根据标签一次性取消所有相关的任务,或者观察它们的状态。
// 构建请求 WorkRequest statsRequest = new OneTimeWorkRequest.Builder(StatsWorker.class) // 1. 设置初始延迟:注册成功 5 分钟后才真正开始跑 .setInitialDelay(5, TimeUnit.MINUTES) // 2. 打上标签:方便后续管理 .addTag("user_onboarding_tasks") .addTag("priority_low") .build(); // 提交任务 WorkManager.getInstance(context).enqueue(statsRequest); // --- 稍后在其他地方(如退出登录时)--- // 3. 通过标签一次性取消所有相关任务 WorkManager.getInstance(context).cancelAllWorkByTag("user_onboarding_tasks");2.2.3 约束条件
这是 WorkManager 最强大的地方。你可以定义 Constraints 对象并传给请求:
Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) // 需要网络连接 .setRequiresBatteryNotLow(true) // 电量不低 .setRequiresCharging(false) // 不需要充电 .setRequiresDeviceIdle(false) // 设备不需要空闲 .setRequiresStorageNotLow(true) // 存储空间充足 .build(); OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(MyUploadWorker.class) .setConstraints(constraints) .build();| 约束项 | 说明 |
setRequiredNetworkType | 比如CONNECTED(有网)、UNMETERED(仅 Wi-Fi)。 |
setRequiresCharging | 是否必须连接电源。 |
setRequiresDeviceIdle | 是否在设备空闲时运行(适合 CPU 密集型任务)。 |
setRequiresBatteryNotLow | 电量不足时是否停止。 |
2.2.4 输入数据
正如我们之前讨论的,通过setInputData(Data)传递键值对。
Data inputData = new Data.Builder() .putString("user_id", "12345") .putInt("file_count", 10) .putStringArray("file_paths", new String[]{"/path/1", "/path/2"}) .build(); OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyUploadWorker.class) .setInputData(inputData) .build();2.2.5 添加优先级
这是 WorkManager 2.7 引入的最接近“高优先级”的机制。它告诉系统:这个任务非常重要,应该尽快执行,不受系统能耗优化(如 App Standby Buckets)的严格限制。
OneTimeWorkRequest highPriorityWork = new OneTimeWorkRequest.Builder(MyUploadWorker.class) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build();适用场景:用户触发的必须立刻看到结果的操作(如发送消息、同步重要通知)。
在setExpedited中,OutOfQuotaPolicy枚举主要提供以下两个参数
- RUN_AS_NON_EXPEDITED_WORK_REQUEST:如果加急配额用尽,任务不会被取消,而是会降级为普通的后台任务。
- DROP_WORK_REQUEST:如果加急配额用尽,直接丢弃这个任务请求。
2.3 提交任务 WorkManager
既然我们已经定义好了“工人”(Worker)和“合同”(WorkRequest),现在就到了执行阶段:把请求交给WorkManager
WorkManager workManager = WorkManager.getInstance(context); workManager.enqueue(uploadWorkRequest);- WorkManager 是一个单例,你需要通过Context来获取它。
- 如果你不在乎任务是否重复,直接使用enqueue方法:
2.3.1 提交唯一任务
有时候你不想重复提交同一个任务(比如点击多次“同步”按钮)。你可以通过WorkManager.enqueueUniqueWork()来管理唯一任务:
workManager.enqueueUniqueWork( "unique_upload_name", // 任务的唯一标识名称 ExistingWorkPolicy.REPLACE, // 如果任务已存在,怎么处理?(REPLACE, KEEP, APPEND) (OneTimeWorkRequest) uploadRequest );KEEP:如果已有相同名称的任务在排队,则保留旧的,忽略新的。
REPLACE:用新的替换旧的。
APPEND:将新任务排在旧任务后面(形成链)。
2.3.2 获取任务状态
在提交任务后,我们通常需要知道任务进行得怎么样了。WorkManager 提供了一套非常强大的机制,让你可以通过任务的 id、tag 或 name 来实时监控任务的状态。
WorkManager workManager = WorkManager.getInstance(context); workManager.getWorkInfoByIdLiveData(workRequest.getId()) .observe(this, workInfo -> { if (workInfo != null) { WorkInfo.State state = workInfo.getState(); Data outputData = workInfo.getOutputData(); switch (state) { case ENQUEUED: Log.d("TAG", "任务已入队"); break; case RUNNING: Log.d("TAG", "任务执行中"); break; case SUCCEEDED: Log.d("TAG", "任务成功"); break; case FAILED: Log.d("TAG", "任务失败"); break; case BLOCKED: Log.d("TAG", "任务被阻塞"); break; case CANCELLED: Log.d("TAG", "任务已取消"); break; } } });(1) 任务状态 (WorkInfo)
当你查询任务状态时,你会得到一个 WorkInfo 对象。它包含了任务当前的所有关键信息:
State (状态):任务是正在排队 (ENQUEUED)、运行中 (RUNNING)、已成功 (SUCCEEDED)、已失败 (FAILED),还是被取消了 (CANCELLED)。
Output Data (输出数据):如果任务执行成功并返回了结果,你可以从这里拿。
Tags (标签):任务关联的标签。
Run Attempt Count (运行次数):如果任务失败重试过,这里记录了次数。
你可以根据提交任务的方式选择不同的获取路径:
(2) 通过 ID 获取(最精确)
当你提交 WorkRequest 时,每个请求都有一个唯一的 UUID。
// 假设你保存了请求的 ID UUID workId = uploadRequest.getId(); // 使用 LiveData 实时观察 WorkManager.getInstance(context) .getWorkInfoByIdLiveData(workId) .observe(lifecycleOwner, workInfo -> { if (workInfo != null) { System.out.println("当前任务状态: " + workInfo.getState()); } });(3) 通过 Unique Name 获取(最常用)
如果你使用了enqueueUniqueWork,直接用名字查更方便。
WorkManager.getInstance(context) .getWorkInfosForUniqueWorkLiveData("sync_logs") .observe(lifecycleOwner, workInfos -> { // 注意:唯一任务返回的是一个 List<WorkInfo> for (WorkInfo info : workInfos) { // 处理状态 } });在使用 getWorkInfosForUniqueWorkLiveData(String uniqueWorkName) 时,即便你认为只运行了一个任务,WorkManager 依然返回一个 List<WorkInfo>。
- 既可以指派给单个任务,也可以指派给一整个任务链。
(4) 通过 Tag 观察任务状态
WorkManager.getInstance(context) // 注意:通过 Tag 查询返回的是一个 LiveData<List<WorkInfo>> // 因为一个 Tag 可能对应多个任务请求 .getWorkInfosByTagLiveData("sync_logs") .observe(lifecycleOwner, workInfos -> { if (workInfos == null || workInfos.isEmpty()) { return; } for (WorkInfo workInfo : workInfos) { WorkInfo.State state = workInfo.getState(); System.out.println("任务 ID: " + workInfo.getId() + " 状态: " + state); // 如果你对某个特定状态感兴趣 if (state == WorkInfo.State.SUCCEEDED) { // 处理成功后的逻辑 } } });2.3.3 取消任务
// 按 ID 取消 workManager.cancelWorkById(workRequest.getId()); // 按标签取消 workManager.cancelAllWorkByTag("upload_tag"); // 取消所有任务 workManager.cancelAllWork();及时取消不再需要的任务对节省电量和内存至关重要。不过,取消操作并不是“瞬间杀掉”进程,它有一些非常关键的底层逻辑:
当你调用 cancel相关方法时:
- 如果任务还在排队(ENQUEUED),它会直接变成CANCELLED状态,永远不会被执行。
- 如果任务正在运行(RUNNING),WorkManager 会向你的Worker 发送一个信号。
这是最容易被忽视的一点。WorkManager 无法强行停止你的 Java 代码执行(除非进程被杀),它通过isStopped()标志来通知你。
在任务链(A -> B -> C)中,取消行为具有“连带效应”:
- 如果你取消了 任务 A,那么依赖它的任务 B 和 任务 C 也会自动被标记为 CANCELLED。
- 这非常智能,因为 WorkManager 知道 A 没成功,后面的步骤通常已经没有意义了。
如果你使用的是 enqueueUniqueWork,除了手动调用 cancelUniqueWork("name"),你还可以通过提交一个同名但策略为 REPLACE的新任务来隐式地取消并替换掉旧任务。
| 取消方式 | 适用场景 |
cancelWorkById | 精确控制,比如用户点击了某个特定文件的“取消上传”按钮。 |
cancelAllWorkByTag | 批量操作,比如关闭了某个功能模块,需要停掉该模块下所有的后台任务。 |
cancelUniqueWork | 业务逻辑控制,确保某个命名的业务流程彻底停止。 |
三、任务链和复杂工作流
它能让你把多个简单的 Worker像乐高积木一样组合成复杂的业务流。
3.1 串行执行
最简单的形式是:做完 A,再做 B,最后做 C。
WorkManager workManager = WorkManager.getInstance(context); OneTimeWorkRequest workA = new OneTimeWorkRequest.Builder(WorkerA.class).build(); OneTimeWorkRequest workB = new OneTimeWorkRequest.Builder(WorkerB.class).build(); OneTimeWorkRequest workC = new OneTimeWorkRequest.Builder(WorkerC.class).build(); // A -> B -> C 顺序执行 workManager.beginWith(workA) .then(workB) .then(workC) .enqueue();特点:如果workA 失败,整个链条会停止,B 和 C 都不会运行。
3.2 并行执行
// 同时执行 A 和 B,然后执行 C workManager.beginWith(workA, workB) .then(workC) .enqueue();在这个场景中,workA 和 workB 会被同时放入调度队列。只有当它们两个都成功完成(返回 Result.success())时,workC 才会触发执行。
当 workA 和 workB 并行运行并分别返回数据时,workC 接收到的输入数据(Input Data)会是两者的并集。
- 如果 workA 返回 {"key1": "value1"}
- 如果 workB 返回 {"key2": "value2"}
- 那么 workC 通过 getInputData() 就能同时拿到 key1 和 key2。
3.3 组合任务
除了简单的并行,你还可以通过WorkContinuation.combine()把多个已经存在的任务链合并在一起。
OneTimeWorkRequest workA, workB, workC, workD; // A 和 B 并行,完成后执行 C,D 等待 A 完成 WorkContinuation continuation1 = workManager.beginWith(workA, workB); WorkContinuation continuation2 = workManager.beginWith(workC); WorkContinuation continuation3 = workManager.beginWith(workD); WorkContinuation combined = WorkContinuation .combine(continuation1, continuation2, continuation3) .then(workE); combined.enqueue();