WorkManager的调度机制:任务队列与执行策略

这一节,我们来深入看看WorkManager的内部调度机制。说白了,就是搞清楚你的任务到底是怎么被排队、怎么被执行的。

我早年用JobScheduler的时候最头疼一件事:任务一多,执行时机完全不可控。WorkManager把这件事抽象得漂亮多了。它的核心调度架构,我画了张图帮你理解。

WorkManager 调度机制结构图 任务提交 内部任务队列 (WorkSpec) 约束条件检查 调度器选择 Worker 执行器 UI 主线程 结果回调 / LiveData 帧率抖动风险点 任务在主线程执行 约束条件反复检查

内部调度器:谁来管我的任务?

WorkManager背后有两套调度方案:JobScheduler(API 23+)和Firebase Job Dispatcher(旧版兼容)。

我个人习惯重点关注JobScheduler这条路,因为90%的线上设备都走这条线。它的逻辑其实不复杂:

  • 任务提交:你调用enqueue()时,WorkManager不会立刻执行。它先把任务信息写入本地数据库(Room),这叫WorkSpec。
  • 队列维护:内部维护一个基于WorkSpec的队列,按优先级和约束条件排序。
  • 调度触发:JobScheduler收到信号后,根据系统空闲、电量、网络状态等决定什么时候拉起你的Worker。
关键认知: WorkManager是“声明式”调度。你只管说“我想在什么条件下干活”,它来安排时机。别指望它像ThreadPoolExecutor一样立即执行。

任务链:beginWith 和 then 的秘密

你想想看,如果三个任务之间有依赖关系,比如先下载配置、再解析、最后展示,该怎么办?

WorkManager提供了beginWith()和then()来构建任务链。这背后其实是一个有向无环图(DAG)调度器。

val workA = OneTimeWorkRequestBuilder<DownloadWorker>().build()
val workB = OneTimeWorkRequestBuilder<ParseWorker>().build()
val workC = OneTimeWorkRequestBuilder<DisplayWorker>().build()

WorkManager.getInstance(context)
    .beginWith(workA)
    .then(workB)
    .then(workC)
    .enqueue()

嗯,这里要注意:then()加进去的任务不会立即执行,它们会被标记为“等待前驱完成”。只有前一个任务返回Result.success(),后一个才会被调度器放入就绪队列。

我的经验: 曾经有个项目,我在链上挂了5个任务,结果第三个任务执行了8秒。后面两个任务一直卡着,UI线程也被轮询结果影响,掉帧严重。现在我会给每个长任务单独设置setBackoffCriteria(),并控制链长不超过3个。

任务组合:合并与冲突解决

还有一种场景:多个输入任务完成后,合并成一个输出任务。比如先并行下载图片和文字,然后合成页面。

val imageWork = OneTimeWorkRequestBuilder<ImageDownloadWorker>().build()
val textWork = OneTimeWorkRequestBuilder<TextDownloadWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(imageWork, textWork))
    .then(composWork)
    .enqueue()

这里beginWith(listOf(...))会让两个任务并行执行。内部调度器会创建两个独立的WorkContinuation,然后通过一个合并节点等待它们都完成。

如果两个任务的ID恰好相同呢?WorkManager默认的冲突解决策略是KEEP(保留已有任务),还有REPLACE(替换)。你可以用ExistingWorkPolicy控制:

策略作用适用场景
KEEP如果队列中有相同ID的任务,新提交的被忽略缓存刷新、定时同步
REPLACE删除旧任务,用新任务替代用户手动刷新数据
APPEND新任务追加到旧任务之后执行队列操作任务
APPEND_OR_REPLACE如果旧任务还没执行,替换它;否则追加复杂的链式操作

约束条件:它会卡住UI吗?

这是面试的常考点,也是帧率抖动的重灾区。WorkManager的约束条件包括:

  • setRequiredNetworkType(NetworkType.CONNECTED):需要网络
  • setRequiresCharging(true):需要在充电状态下
  • setRequiresBatteryNotLow(true):电量不能低
  • setRequiresStorageNotLow(true):存储空间充足

这些约束条件,WorkManager底层是通过系统广播接收器和JobScheduler的约束回调来检查的。你想想看,频繁的广播监听会不会影响UI帧率?

答案是:会。尤其是NetworkType.CONNECTED,网络切换时会触发大量回调。我之前在日志里看到过这种场景:WiFi断开重连,不到1秒内约束条件被重新检查了5次,每次检查都触发了Room数据库查询,那个查询效率不够高的话,就会挤占UI线程的CPU时间。

避坑指南: 我曾经在一个直播App里发现,主播上传视频时WorkManager不停检查网络状态,导致滑屏帧率从60掉到40。后来我把约束条件改成了NetworkType.UNMETERED(非蜂窝网络),只在WiFi下执行上传,并且加了setBackoffCriteria()减少重试频率。问题才解决。

所以,我的建议是:不要滥用约束条件。如果任务必须立即执行,就别设约束,或者用immediate()方式提交。如果任务可以等,那就把约束设宽松点,给系统留足调度窗口。

总结一下今天的内容:WorkManager的调度机制本质上是一个基于数据库的、声明式的任务队列系统。它不直接管理线程,而是通过JobScheduler和约束条件系统来间接控制执行时机。理解这张流程图,你就能在写任务时提前预判:它什么时候会阻塞?什么时候会抖动?