第35章:WorkManager:后台任务调度、约束条件、链式任务、观察任务状态

后台任务,说白了就是让App在用户看不见的时候偷偷干活。比如上传日志、同步数据、定期清理缓存。Android上做后台任务的方式很多,但WorkManager是我目前最推荐的选择。

为什么?因为它能保证任务一定会执行,哪怕App被杀死、手机重启。我早期做项目时用过Service + AlarmManager的组合,结果各种机型兼容性问题搞得我焦头烂额。后来换成WorkManager,世界清净了。

35.1 WorkManager 的核心概念

WorkManager 是 Android Jetpack 的一部分。它专门处理那些「需要保证完成」的后台任务。你想想看,如果用户上传一张照片到云端,中途App被系统杀掉了,这张照片就丢了?那肯定不行。

WorkManager 会帮你把任务信息存到数据库里。等设备恢复后,它自动重新调度。嗯,这里要注意:它不适用于「即时」任务,比如用户点击按钮后立刻要看到结果。那种场景你应该用协程或者线程池。

适用场景:
  • 数据同步(上传日志、下载资源)
  • 定期清理(删除过期缓存)
  • 条件触发(仅在充电时备份照片)
  • 链式任务(先下载配置,再初始化SDK)

35.2 基本用法:三步搞定一个任务

先看一个最简单的例子。我习惯把每个后台任务定义成一个 Worker 类:

class LogUploadWorker(
    context: Context,
    params: WorkerParameters
) : Worker(context, params) {

    override fun doWork(): Result {
        return try {
            // 模拟上传日志
            Log.d("WorkManager", "开始上传日志...")
            Thread.sleep(2000)
            Log.d("WorkManager", "上传成功")
            Result.success()
        } catch (e: Exception) {
            Log.e("WorkManager", "上传失败", e)
            Result.retry()
        }
    }
}

然后创建请求并提交:

val uploadRequest = OneTimeWorkRequestBuilder<LogUploadWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueue(uploadRequest)

就这么简单。任务会在网络可用时自动执行。如果失败,它会根据重试策略重新尝试。

我的习惯:Result.retry() 配合 setBackoffCriteria() 使用。默认是线性重试,我一般改成指数退避,避免频繁重试把服务器打崩。

35.3 约束条件:让任务更聪明

约束条件就是「任务执行的前提」。我曾经遇到一个坑:用户在地铁里刷App,后台任务突然开始上传大文件,流量哗哗地跑。用户投诉后,我加上了网络类型约束。

WorkManager 支持的约束条件:

约束类型 API 说明
网络状态 setRequiredNetworkType() CONNECTED / UNMETERED / NOT_ROAMING
充电状态 setRequiresCharging() true 表示只在充电时执行
空闲状态 setRequiresDeviceIdle() 设备空闲时执行(API 23+)
存储空间 setRequiresStorageNotLow() 存储空间充足时执行
电池电量 setRequiresBatteryNotLow() 电量不低时执行

组合使用也很简单:

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)  // 仅WiFi
    .setRequiresCharging(true)                      // 充电中
    .setRequiresBatteryNotLow(true)                 // 电量充足
    .build()

嗯,这里要注意:约束条件越多,任务执行的窗口就越窄。如果用户一直不满足条件,任务会一直等待。我建议只加必要的约束,别太苛刻。

35.4 链式任务:按顺序执行

很多业务场景需要任务之间有依赖关系。比如:先下载配置文件,再初始化SDK,最后上报状态。用链式任务就很优雅:

val downloadWork = OneTimeWorkRequestBuilder<ConfigDownloadWorker>()
    .build()

val initWork = OneTimeWorkRequestBuilder<SdkInitWorker>()
    .build()

val reportWork = OneTimeWorkRequestBuilder<StatusReportWorker>()
    .build()

WorkManager.getInstance(context)
    .beginWith(downloadWork)
    .then(initWork)
    .then(reportWork)
    .enqueue()

WorkManager 会保证顺序执行。如果中间某个任务失败了,后面的任务不会执行。你可以通过 Result.success() 和 Result.failure() 来控制流程。

我曾经踩过的坑:链式任务中如果某个 Worker 返回 Result.retry(),整个链会重新从第一个任务开始执行。所以如果你的链很长,建议把重试逻辑放在单个 Worker 内部处理,而不是依赖 WorkManager 的重试机制。

链式任务还支持「并行 + 串行」的混合模式:

// 先并行下载两个文件
val downloadA = OneTimeWorkRequestBuilder<DownloadWorkerA>().build()
val downloadB = OneTimeWorkRequestBuilder<DownloadWorkerB>().build()

// 等两个都完成后,再合并处理
val mergeWork = OneTimeWorkRequestBuilder<MergeWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(downloadA, downloadB))
    .then(mergeWork)
    .enqueue()

你想想看,这种写法比手动管理线程池和回调要清晰多少?

35.5 观察任务状态:LiveData 与 Flow

任务提交后,我们总得知道它执行得怎么样了。WorkManager 提供了两种观察方式。

方式一:LiveData

val workManager = WorkManager.getInstance(context)
val workId = uploadRequest.id

workManager.getWorkInfoByIdLiveData(workId)
    .observe(this) { workInfo ->
        if (workInfo != null) {
            when (workInfo.state) {
                WorkInfo.State.ENQUEUED -> Log.d("状态", "等待执行")
                WorkInfo.State.RUNNING -> Log.d("状态", "正在执行")
                WorkInfo.State.SUCCEEDED -> Log.d("状态", "执行成功")
                WorkInfo.State.FAILED -> Log.d("状态", "执行失败")
                WorkInfo.State.CANCELLED -> Log.d("状态", "已取消")
                WorkInfo.State.BLOCKED -> Log.d("状态", "被阻塞(依赖的任务未完成)")
            }
        }
    }

方式二:Flow(推荐)

我个人更喜欢用 Flow,因为它和协程配合更自然:

lifecycleScope.launch {
    workManager.getWorkInfoByIdFlow(workId)
        .collect { workInfo ->
            // 处理状态变化
            updateUI(workInfo.state)
        }
}

为什么推荐 Flow?因为你可以用 flow 的各种操作符。比如只关心成功或失败的状态:

workManager.getWorkInfoByIdFlow(workId)
    .filter { it.state.isFinished }
    .collect { workInfo ->
        // 任务结束了,不管成功还是失败
    }

35.6 知识体系总览

我把本章的核心逻辑画了一张图,方便你理解 WorkManager 的整体架构:

WorkManager 核心架构 Worker 类 定义具体任务逻辑 WorkRequest 配置约束、重试策略 Constraints 网络、充电、空闲等 WorkManager.enqueue() 提交任务到调度队列 单任务 独立执行一个Worker 链式任务 beginWith().then() 并行任务 beginWith(listOf()) 观察状态:LiveData / Flow

35.7 避坑指南与最佳实践

最后分享几个我实际项目中积累的经验:

  1. 不要用 WorkManager 做即时任务。它内部有数据库写入和调度开销,至少几百毫秒。用户点击按钮后要立刻反馈的场景,请用协程。
  2. 定期任务用 PeriodicWorkRequest。但要注意最小间隔是15分钟。如果你需要更精确的定时,考虑 AlarmManager。
  3. 任务结果传递。Worker 之间传递数据用 outputData。我习惯把关键结果放到 outputData 里,方便下游任务读取。
  4. 测试很重要。WorkManager 提供了 TestListenableWorkerBuilder,可以方便地写单元测试。别偷懒,我吃过亏。
我曾经踩过的坑:有一次我写了一个日志上传的 Worker,忘记加网络约束。结果用户在飞机上打开App,后台疯狂重试上传,导致手机发烫。从那以后,我每个 Worker 都会检查约束条件是否合理。

好了,WorkManager 的核心用法就这些。它不像 RxJava 那样复杂,也不像 AlarmManager 那样原始。说白了,它就是 Android 官方给开发者准备的一个「靠谱的后台任务管家」。你只要把任务定义好、约束配好、链式搭好,剩下的交给它就行。


公众号:蓝海资料掘金营,微信deep3321