A
第 18 章JAVA45 分钟

WorkManager 任务调度

掌握 WorkManager 的优势、OneTimeWorkRequest/PeriodicWorkRequest、Constraints(网络/充电/空闲)、链式任务(then/combine)、Worker 类实现、输入输出数据与测试

学习目标

  • 理解 WorkManager 相对 Service 与 JobScheduler 的优势与适用场景
  • 用 OneTimeWorkRequest 与 PeriodicWorkRequest 调度单次/周期任务
  • 用 Constraints 约束网络/充电/空闲/存储空间
  • 构建 then 链式与 combine 并行组合的任务图
  • 实现自定义 Worker、传递输入输出数据并完成单元测试

学习目标

WorkManager 是 Android Jetpack 提供的“可持久化、可约束、可链式”的任务调度库,自动根据设备 API 与状态选择 JobScheduler / AlarmManager / BroadcastReceiver 实现。本章目标:

  • 理解 WorkManager 替代 Service / JobScheduler / AlarmManager 的优势;
  • 用 OneTimeWorkRequest 与 PeriodicWorkRequest 调度任务;
  • 用 Constraints 约束网络、充电、空闲、存储空间;
  • 用 then 与 combine 构建串行/并行任务图;
  • 实现自定义 Worker、传递输入输出数据,并完成单元测试。

WorkManager 优势

特性 Service JobScheduler WorkManager
任务持久化 否 API 23+ 是(进程被杀后任务保留)
重启后任务恢复 否 是 是
约束条件 手动判断 API 23+ 全版本统一
链式组合 手动 部分 内置
API 兼容范围 全部 API 23+ API 14+
后台执行限制豁免 否 是 是

适用场景:

  • 上传/下载文件:可中断可恢复;
  • 本地数据同步:要求 WiFi、充电时执行;
  • 日志上报、清理缓存:周期性任务;
  • 业务流程编排:A 完成后执行 B,B 失败后回滚 A。

不适用:

  • 需要立即执行(用户在前台等待)→ 用协程或线程池;
  • 需要精确时间(如闹钟)→ 用 AlarmManager(精确闹钟)。

Gradle 依赖

dependencies {
    def work_version = "2.9.1"
    implementation "androidx.work:work-runtime:$work_version"
    // 可选:可选 GCMNetworkManager 兼容(多用于 Google Play 设备优化)
    implementation "androidx.work:work-gcm:$work_version"
    // 测试
    androidTestImplementation "androidx.work:work-testing:$work_version"
}

自定义 Worker

1. 同步 Worker(不推荐耗时操作)

public class CompressWorker extends Worker {

    public CompressWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public Result doWork() {
        String inputPath = getInputData().getString("input_path");
        if (TextUtils.isEmpty(inputPath)) {
            return Result.failure();
        }
        try {
            compress(inputPath);
            Data output = new Data.Builder()
                    .putString("result_path", inputPath + ".zip")
                    .build();
            return Result.success(output);
        } catch (Exception e) {
            return Result.retry();
        }
    }

    private void compress(String path) {
        // ...
    }
}

2. Coroutine / Rx Worker(推荐异步)

由于本章语言为 Java,下面用 ListenableWorker + ListenableFuture(Guava)实现异步:

implementation "androidx.work:work-runtime:$work_version"
implementation "com.google.guava:guava:33.3.1-android"
public class UploadWorker extends ListenableWorker {

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

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        return CallbackToFutureAdapter.getFuture(resolver -> {
            String filePath = getInputData().getString("file_path");
            UploadTask task = new UploadTask(filePath, new UploadTask.Callback() {
                @Override
                public void onSuccess(String url) {
                    Data output = new Data.Builder()
                            .putString("result_url", url)
                            .build();
                    resolver.set(Result.success(output));
                }

                @Override
                public void onFailure() {
                    resolver.set(Result.retry());
                }
            });
            // 持有引用以便取消
            resolver.addCancellationListener(task::cancel, Runnable::run);
            task.start();
            return task;
        });
    }
}

OneTimeWorkRequest

1. 构建请求

Constraints constraints = new Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED) // WiFi
        .setRequiresCharging(true)
        .setRequiresBatteryNotLow(true)
        .setRequiresStorageNotLow(true)
        .build();

Data input = new Data.Builder()
        .putString("file_path", "/sdcard/file.jpg")
        .putLong("file_size", 102400)
        .build();

OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(UploadWorker.class)
        .setConstraints(constraints)
        .setInputData(input)
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
        .setInitialDelay(10, TimeUnit.MINUTES)
        .addTag("upload")
        .build();

2. 入队与观察

WorkManager.getInstance(this)
        .enqueue(uploadWork);

// 观察
WorkManager.getInstance(this)
        .getWorkInfoByIdLiveData(uploadWork.getId())
        .observe(this, workInfo -> {
            if (workInfo != null) {
                if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
                    String url = workInfo.getOutputData().getString("result_url");
                    // ...
                } else if (workInfo.getState() == WorkInfo.State.FAILED) {
                    // ...
                }
            }
        });

// 取消
WorkManager.getInstance(this).cancelWorkById(uploadWork.getId());

3. 唯一任务

WorkManager.getInstance(this).enqueueUniqueWork(
        "unique_upload", ExistingWorkPolicy.KEEP, uploadWork);

ExistingWorkPolicy 选项:

  • REPLACE:替换旧任务;
  • KEEP:若已有同名任务在排队则保留旧的;
  • APPEND:追加(不取消旧的);
  • APPEND_OR_REPLACE:追加并允许替换已完成的同名任务。

PeriodicWorkRequest

PeriodicWorkRequest syncWork = new PeriodicWorkRequest.Builder(
        SyncWorker.class, 15, TimeUnit.MINUTES)
        .setConstraints(constraints)
        .addTag("periodic_sync")
        .build();

WorkManager.getInstance(this).enqueueUniquePeriodicWork(
        "periodic_sync",
        ExistingPeriodicWorkPolicy.KEEP,
        syncWork);

注意:最小周期为 15 分钟,不能更短。setInitialDelay 可用于“首次延迟”。

链式任务

1. 串行:then

WorkManager.getInstance(this)
        .beginWith(filterWorker)
        .then(compressWorker)
        .then(uploadWorker)
        .enqueue();

链中前一个 Worker 的输出会自动作为下一个的输入。

2. 并行:combine

Continuation chain = WorkManager.getInstance(this)
        .beginWith(Arrays.asList(filterWorkerA, filterWorkerB))
        .then(combineWorker);

chain.enqueue();

两个 filter 并行执行,全部成功后才进入 combineWorker。

3. 复杂图:结合 then 与 combine

WorkContinuation left = WorkManager.getInstance(this)
        .beginWith(filterWorkerA).then(compressWorkerA);

WorkContinuation right = WorkManager.getInstance(this)
        .beginWith(filterWorkerB).then(compressWorkerB);

WorkContinuation.combine(Arrays.asList(left, right))
        .then(uploadWorker)
        .enqueue();

4. 唯一链

WorkManager.getInstance(this)
        .beginUniqueWork("image_pipeline",
                ExistingWorkPolicy.KEEP,
                filterWorker)
        .then(compressWorker)
        .then(uploadWorker)
        .enqueue();

输入输出数据

Data 是 WorkManager 传递数据的核心容器,支持基本类型、String 与 Parcelable(受限)。不要传递大对象,Data 有 10KB 限制。

Data input = new Data.Builder()
        .putString("user_id", "u_1001")
        .putInt("retry_count", 3)
        .putBoolean("use_cache", true)
        .build();

// Worker 中获取
String userId = getInputData().getString("user_id");

// 返回
Data output = new Data.Builder()
        .putString("result_url", "https://...")
        .build();
return Result.success(output);

Worker 测试

androidTestImplementation "androidx.work:work-testing:2.9.1"
androidTestImplementation "androidx.test:core:1.6.1"
androidTestImplementation "androidx.test.ext:junit:1.2.1"

1. 同步 Worker 单元测试

@RunWith(AndroidJUnit4.class)
public class CompressWorkerTest {

    @Test
    public void compress_success() {
        Data input = new Data.Builder()
                .putString("input_path", "/sdcard/test.jpg")
                .build();
        TestWorkerBuilder workerBuilder = TestWorkerBuilder.from(
                ApplicationProvider.getApplicationContext(),
                CompressWorker.class);
        workerBuilder.setInputData(input);

        CompressWorker worker = (CompressWorker) workerBuilder.build();
        ListenableWorker.Result result = worker.startWork().get();
        assertTrue(result instanceof ListenableWorker.Result.Success);
    }
}

2. WorkManager 测试驱动

@RunWith(AndroidJUnit4.class)
public class WorkFlowTest {

    @Before
    public void setup() {
        Context context = ApplicationProvider.getApplicationContext();
        Configuration config = new Configuration.Builder()
                .setMinimumLoggingLevel(Log.DEBUG)
                .setExecutor(SynchronousExecutor.create())
                .build();
        WorkManagerTestInitHelper.initializeTestWorkManager(context, config);
    }

    @Test
    public void chain_runs() throws Exception {
        Context context = ApplicationProvider.getApplicationContext();
        OneTimeWorkRequest filter = new OneTimeWorkRequest.Builder(FilterWorker.class).build();
        OneTimeWorkRequest compress = new OneTimeWorkRequest.Builder(CompressWorker.class).build();

        WorkManager.getInstance(context)
                .beginWith(filter).then(compress).enqueue().getResult().get();
        // TestDriver 可模拟约束满足
    }
}

常见坑与最佳实践

  1. 传递大对象:Data 限制约 10KB。文件路径传 ID,Worker 内查数据库加载。
  2. PeriodicWorkRequest 周期 < 15 分钟:会被强制截断为 15 分钟。
  3. 重复 enqueueUniqueWork 用错 Policy:KEEP 会让旧任务永远不被取消;REPLACE 会取消正在进行的任务。按业务谨慎选择。
  4. Result.retry() 无限循环:必须配置 setBackoffCriteria 与最大重试次数(自己实现计数器写入 Data)。
  5. 在 Worker 中持有 Activity/View 引用:Worker 可能跨进程切换,会泄漏。只持有 Application Context。
  6. 依赖 LiveData 观察 Worker:在 Activity 销毁后回调仍执行更新 UI。请绑定到 getViewLifecycleOwner() 或在 Worker 完成后用 Notification/广播通知。
  7. 未在 Manifest 注册:WorkManager 默认通过 WorkManagerInitializer ContentProvider 自动初始化,无需手动注册。若需自定义初始化要移除 ContentProvider 并调 WorkManager.initialize。
  8. TaskList 太深导致延迟:链式任务过多时调度开销增大,复杂流程建议拆分多个 UniqueWork。
  9. 测试忘记初始化:必须用 WorkManagerTestInitHelper.initializeTestWorkManager 否则 WorkManager.getInstance 抛异常。
  10. 后台权限:Android 12+ 部分后台触发需要前台上下文(如后台启动 Activity 限制),WorkManager 在前台 Service 中可豁免部分限制。

章节小结

  • WorkManager 提供持久化、约束、链式、API 兼容四大优势,是 Android 后台任务调度首选;
  • OneTimeWorkRequest 处理单次任务,PeriodicWorkRequest 处理最小 15 分钟周期任务;
  • Constraints 约束网络、充电、空闲、存储、电池;
  • then 串行、combine 并行,构建复杂任务图;
  • 自定义 Worker 用 Worker(同步)或 ListenableWorker(异步)+ Data 传递输入输出;
  • WorkManagerTestInitHelper + TestWorkerBuilder 完成 Worker 单元测试。

下一章预告

第 19 章将讲解 BroadcastReceiver 广播:静态注册(清单)与动态注册(registerReceiver)、标准广播与有序广播、自定义广播、LocalBroadcastManager 废弃后的替代方案、广播权限与最佳实践。