一、WorkManager 是什么?为什么我们需要它?

后台任务调度,听起来好像很简单对吧?

但做过几个项目后你就会发现,Android 的后台任务管理其实是个大坑。不同厂商的定制系统、不同版本的 API 限制、还有用户动不动就杀后台的习惯……嗯,我早期做的一个下载模块就吃过这个亏——明明 Service 跑得好好的,一到小米手机上就被干掉了。

WorkManager 就是 Google 为了解决这些破事推出的官方方案。它不只是一个简单的任务执行器,而是一个兼容性极强、能保证任务最终执行的调度框架。

核心特点:

  • 兼容 API 14+,连老设备都能用
  • 即使 App 被杀死,任务也会在条件满足时恢复执行
  • 支持一次性任务、周期性任务、链式任务
  • 可以设置约束条件(如网络状态、电量等)

说白了,你只需要告诉 WorkManager「我要做什么、什么时候做」,剩下的它帮你搞定。

WorkManager 核心架构 Worker doWork() 执行逻辑 WorkRequest 定义任务与约束 WorkManager 调度与执行引擎 约束条件:网络 / 电量 / 存储 / 空闲状态 链式任务:Work A → Work B → Work C

二、快速上手:第一个 WorkManager 任务

先别急着看复杂的东西。我们从一个最简单的例子开始。

2.1 定义 Worker

每个任务的核心逻辑都写在 Worker 类里。你只需要继承 Worker,然后实现 doWork() 方法。

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

    override fun doWork(): Result {
        // 这里写你的后台逻辑
        Log.d("WorkManager", "开始上传文件...")
        
        // 模拟耗时操作
        Thread.sleep(2000)
        
        return Result.success() // 返回成功
        // 或者 Result.failure() 返回失败
        // 或者 Result.retry() 要求重试
    }
}

我个人习惯把 Worker 写得尽量轻量。不要在 doWork() 里搞太复杂的逻辑,拆成多个小任务更稳妥。

2.2 创建 WorkRequest 并调度

val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .build()

WorkManager.getInstance(context).enqueue(uploadRequest)

就这么简单。两行代码,任务就跑起来了。

小提示:OneTimeWorkRequest 是一次性任务。如果你需要重复执行,后面会讲到 PeriodicWorkRequest。

三、约束条件:让任务在合适的时机执行

你想想看,如果用户正在用流量看视频,你的 App 突然在后台下载大文件,是不是很招人烦?

约束条件就是用来解决这个问题的。你可以告诉 WorkManager:「等连上 Wi-Fi 再执行」、「等电量充足再执行」。

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)  // 需要网络
    .setRequiresCharging(true)                      // 需要充电
    .setRequiresBatteryNotLow(true)                 // 电量不能太低
    .setRequiresStorageNotLow(true)                 // 存储空间充足
    .build()

val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .setConstraints(constraints)
    .build()

我在项目中遇到过一个问题:用户反馈某些任务一直没执行。查了半天才发现,是约束条件设置得太严格了——既要充电又要 Wi-Fi,用户白天在公司插着电源但用的是有线网络,结果任务一直挂在那里。后来我把约束放宽了一些,加了个超时兜底。

注意:约束条件不是立即生效的。WorkManager 会在条件满足后的某个时间点执行任务,具体时间取决于系统调度。别指望它像闹钟一样准时。

四、链式任务:一个接一个地执行

实际项目中,很少有单个任务就能搞定的事情。比如上传图片前要先压缩,压缩完还要生成缩略图,最后再上传到服务器。

链式任务就是为这种场景设计的。你可以把多个 WorkRequest 串起来,让它们按顺序执行。

val compressRequest = OneTimeWorkRequestBuilder<CompressWorker>()
    .build()

val thumbnailRequest = OneTimeWorkRequestBuilder<ThumbnailWorker>()
    .build()

val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .build()

WorkManager.getInstance(context)
    .beginWith(compressRequest)           // 第一步:压缩
    .then(thumbnailRequest)               // 第二步:生成缩略图
    .then(uploadRequest)                  // 第三步:上传
    .enqueue()

链式任务还有一个很实用的特性:数据传递。前一个任务的输出可以作为后一个任务的输入。

// 在 Worker 中输出数据
class CompressWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        val output = Data.Builder()
            .putString("compressed_path", "/data/compressed.jpg")
            .build()
        return Result.success(output)
    }
}

// 在下一个 Worker 中获取数据
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        val compressedPath = inputData.getString("compressed_path")
        // 上传 compressedPath 指向的文件
        return Result.success()
    }
}

嗯,这里要注意:Data 对象的大小有限制,最大 10KB。别往里面塞大文件路径以外的数据。

五、周期性任务:定时重复执行

有些任务需要定期执行,比如每天同步一次数据、每周清理一次缓存。这时候就用 PeriodicWorkRequest。

val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    24, TimeUnit.HOURS  // 每24小时执行一次
)
    .setConstraints(constraints)
    .build()

WorkManager.getInstance(context).enqueue(syncRequest)

重要提醒:PeriodicWorkRequest 的最小间隔是 15 分钟。你设置 5 分钟也没用,系统会强制拉回到 15 分钟。这是 Google 为了省电做的限制。

我曾经接手过一个项目,里面用 AlarmManager 做定时任务,结果在 Android 8.0 以上的设备上频繁崩溃。换成 WorkManager 的周期性任务后,问题就解决了。说白了,能用 WorkManager 就别自己造轮子。

六、观察任务状态

任务提交了,怎么知道它执行完了没?WorkManager 提供了 LiveData 或 Flow 来观察状态。

WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(uploadRequest.id)
    .observe(this) { workInfo ->
        if (workInfo != null) {
            when (workInfo.state) {
                WorkInfo.State.SUCCEEDED -> Log.d("WorkManager", "任务成功")
                WorkInfo.State.FAILED -> Log.d("WorkManager", "任务失败")
                WorkInfo.State.RUNNING -> Log.d("WorkManager", "任务执行中")
                WorkInfo.State.ENQUEUED -> Log.d("WorkManager", "任务已排队")
                else -> {}
            }
        }
    }

状态机其实很简单:ENQUEUED → RUNNING → SUCCEEDED/FAILED。如果设置了重试策略,失败后还会回到 ENQUEUED 状态。

七、避坑指南

最后分享几个我踩过的坑:

  • 不要依赖 WorkManager 做即时任务。它设计初衷是「保证最终执行」,不是「立即执行」。需要立即执行的任务请用前台 Service。
  • UniqueWorkPolicy 要慎用。如果你用 beginUniqueWork() 创建链式任务,记得指定冲突策略:KEEP(保留旧任务)还是 REPLACE(替换旧任务)。我一开始没注意,结果重复提交了好几个任务。
  • 测试时记得加延迟。WorkManager 在测试环境下默认是立即执行的。用 TestListenableWorkerBuilder 可以模拟真实调度。
  • 不要忘记处理 Result.retry()。如果任务返回 retry,WorkManager 会根据 BackoffPolicy 重试。默认是指数退避,第一次等 30 秒,第二次等 60 秒,以此类推。

我的习惯:每个 Worker 都加上 try-catch,捕获到异常时返回 Result.retry()。这样即使网络波动或临时错误,任务也能自动恢复。

好了,关于 WorkManager 的核心用法就这些。从一次性任务到链式任务,从约束条件到周期性调度,这套工具基本覆盖了日常开发中 90% 的后台需求。下次遇到后台任务,先想想能不能用 WorkManager 搞定——大概率是可以的。


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