WorkManager的调度机制:任务队列与执行策略
这一节,我们来深入看看WorkManager的内部调度机制。说白了,就是搞清楚你的任务到底是怎么被排队、怎么被执行的。
我早年用JobScheduler的时候最头疼一件事:任务一多,执行时机完全不可控。WorkManager把这件事抽象得漂亮多了。它的核心调度架构,我画了张图帮你理解。
内部调度器:谁来管我的任务?
WorkManager背后有两套调度方案:JobScheduler(API 23+)和Firebase Job Dispatcher(旧版兼容)。
我个人习惯重点关注JobScheduler这条路,因为90%的线上设备都走这条线。它的逻辑其实不复杂:
- 任务提交:你调用
enqueue()时,WorkManager不会立刻执行。它先把任务信息写入本地数据库(Room),这叫WorkSpec。 - 队列维护:内部维护一个基于
WorkSpec的队列,按优先级和约束条件排序。 - 调度触发:JobScheduler收到信号后,根据系统空闲、电量、网络状态等决定什么时候拉起你的Worker。
任务链: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(),后一个才会被调度器放入就绪队列。
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时间。
NetworkType.UNMETERED(非蜂窝网络),只在WiFi下执行上传,并且加了setBackoffCriteria()减少重试频率。问题才解决。
所以,我的建议是:不要滥用约束条件。如果任务必须立即执行,就别设约束,或者用immediate()方式提交。如果任务可以等,那就把约束设宽松点,给系统留足调度窗口。
总结一下今天的内容:WorkManager的调度机制本质上是一个基于数据库的、声明式的任务队列系统。它不直接管理线程,而是通过JobScheduler和约束条件系统来间接控制执行时机。理解这张流程图,你就能在写任务时提前预判:它什么时候会阻塞?什么时候会抖动?