先把结论放在前面:WorkManager 解决的是"保证要做成、且尊重系统调度"的问题,不是"立刻执行"的问题。 它的核心能力只有三样——持久化任务队列、约束条件(Constraints)、可组合的工作链(WorkSpec)。选型判据就一句:需要保证完成、可以延迟的工作交给它;需要立刻且用户可见的交给前台服务。
每次面试讲到 WorkManager,都有人栽在追问上。这篇把追问最密集的地方提前标出来。
先把 API 表面过一遍,重点在约束
val request = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 联网
.setRequiresCharging(false)
.setRequiresStorageNotLow(true) // 存储空间充足
.setRequiresBatteryNotLow(true) // 电量充足
.setRequiredNetworkType(NetworkType.UNMETERED) // 覆盖上一条:非计量网络
.setContentUriTriggers( // 内容变化触发
listOf(ContentUriObserver(Uri.parse("content://media/external/images/media")))
)
.setRequiresDeviceIdle(false) // 需设备空闲(仅 API 23+ 且省电模式)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) // 失败重试策略
.addTag("upload")
.build()
// 入队:唯一名 + 替换策略,避免重复提交
WorkManager.getInstance(context).enqueueUniqueWork(
"upload_$taskId",
ExistingWorkPolicy.KEEP, // 已有同名任务则忽略本次
request
)
// 观察结果
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(request.id)
.observe(this) {
info ->
when (info?.state) {
WorkInfo.State.SUCCEEDED -> {
/* 成功 */ }
WorkInfo.State.FAILED -> {
/* 失败 */ }
WorkInfo.State.RUNNING -> {
/* setProgress */ }
else -> {
/* ENQUEUED / BLOCKED / CANCELLED */ }
}
}
ExistingWorkPolicy 三个值必须能说出来:① KEEP — 已有未完成同名单任务则忽略本次(防重复提交);② REPLACE — 取消旧的、执行新的(用于"改配置后重做");③ APPEND_OR_REPLACE — 追加到链尾或作为链首替换(链式场景)。
setBackoffCriteria 的两个策略:EXPONENTIAL(指数退避,默认 30 秒起,上限 5 小时)与 LINEAR(线性)。这个重试是系统级的持久重试,进程死了也不丢。
最常见的坑是
把实时聊天心跳交给 WorkManager,周期与准确性都得不到满足。
WorkManager 的最小周期是 15 分钟,且实际执行时间由系统按电量、网络、Doze 状态综合决定,没有任何准时保证。用它做心跳,表现为"有时候连着来两次,有时候半小时不来"。反过来,很多人用它做"上传日志"这种需要尽快完成的事,结果因为在 NetworkType.NOT_REQUIRED 的约束下被系统推迟到充电时才执行,运营看着数据没上来以为功能坏了。
选型判据写成一张表:
| 需求 | 正确方案 |
|---|---|
| 每天一次,允许延迟 | WorkManager + 周期任务 |
| 网络恢复后上传草稿 | WorkManager + CONNECTED 约束 |
| 登录失败后重试 3 次 | WorkManager + 指数退避 |
| 用户点击后立刻开始上传并看到进度 | startForegroundService |
| 聊天心跳、实时同步 | 推送(FCM 长连接)或前台服务 |
| 播放、导航、录音 | 前台服务(对应 foregroundServiceType) |
修法是把职责切回去:WorkManager 负责"保证最终成功的对账与补偿",前台服务或推送负责"实时的那条链路"。两者配合而非二选一——这才是工程上正确的答案。
还有一个更隐蔽的坑:WorkManager 的实践经验是前提先声明,失败路径给兜底,事故挡在上线前。落地清单:① Worker.doWork() 里的耗时必须受 isStopped 检查约束,收到取消要尽快返回;② setProgress 只给 setForegroundAsync 的前台通知用,普通 Worker 里调用无效果(容易误用);③ 失败不能吞异常——Result.failure() 会触发重试,吞掉则任务标记成功但实际没做完;④ 任务参数要序列化安全,Data 超过 10KB 会被拒绝。
Worker 的正确写法
Worker 有两个变体,选错是常见扣分点:
// 1) 同步 Worker:任务在调用线程上跑(一般是后台线程池)
class UploadWorker(ctx: Context, params: WorkerParameters) :
CoroutineWorker(ctx, params) {
// 协程版,doWork 是 suspend
override suspend fun doWork(): Result {
val id = inputData.getString(KEY_ID) ?: return Result.failure()
val upload = repo.upload(id) // 挂起函数,天然走协程调度
upload.await()
return if (upload.isSuccessful) Result.success() else Result.retry()
}
companion object {
const val KEY_ID = "task_id"
}
}
// 2) ListenableWorker + CallbackToFutureAdapter:回调式 SDK 用它
class FusedLocationWorker(ctx: Context, params: WorkerParameters) :
ListenableWorker(ctx, params) {
override fun startWork(): ListenableFuture<Result> =
CallbackToFutureAdapter.getFuture {
future ->
// SDK 的回调式 API 包成 future
sdk.requestLocation {
result -> future.set(result) }
}
}
val data = Data.Builder().putString(KEY_ID, id).build()
OneTimeWorkRequestBuilder<UploadWorker>().setInputData(data).build()
三个高频追问的答案要点:
问:Worker 怎么知道该干哪件事? Worker 靠类名被反射实例化,任务类型靠"类 + inputData 参数"区分。多个 Worker 通过 inputData 里的 KEY_* 常量做分发,这是推荐做法(比在 Worker 里 when(id) 分支更清晰,也便于测试)。
问:Result.retry() 和 Result.failure() 有什么区别? retry() 触发指数退避重试(配合 setBackoffCriteria),适合"网络抖动"这类瞬时失败;failure() 标记终态失败并让后续 WorkInfo 带 outputData 里的错误信息,适合"参数非法""业务拒绝"这类永久失败。Result.failure() 里的错误信息要通过 Data 传给观察者。
问:多个任务怎么串起来? 用 beginUniqueWork + Then() / ThenCombine / Failure():
WorkManager.getInstance(context)
.beginUniqueWork("upload_flow", ExistingWorkPolicy.REPLACE, uploadRequest)
.then(notifyRequest) // 串行
.thenCombine(statsRequest) {
_, _ -> Result.success() } // 并行后串行
.enqueue()
在进入追问之前,先把 WorkManager 在"调度方案全景"里的位置讲清楚——这决定了它该被用在哪。
| 方案 | 特点 | 适用 |
|---|---|---|
Handler.postDelayed / 定时器 |
进程内,进程死就没了 | 进程内的短延迟提示 |
AlarmManager |
进程外可唤醒,精确但对后台限制敏感 | 需要精确到点的闹钟类业务(且用户可见) |
JobScheduler |
系统按条件调度,Doze 下仍可运行 | 需要系统条件配合的长任务 |
WorkManager |
在 JobScheduler 之上加了持久化队列与易用 API | 需要保证完成的可延迟工作 |
| 前台服务 | 用户可见,立即执行 | 播放、导航、录音、即时上传 |
| FCM 推送 | 由服务端触发,跨设备 | 事件驱动的即时通知 |
把这张表讲出来,面试官就知道你不只是会用 enqueueUniqueWork,而是理解它为什么长成这样。WorkManager 的核心价值就是"在 JobScheduler 之上补了持久化与重试"——它自己不调度,只是把任务规格持久化,再把调度委托给系统。
再补一个实际项目里最容易被忽略的维度:可观测性。后台任务最大的问题是"失败了没人知道"。规范做法有三层——① 任务状态上报:把 WorkInfo.State 与失败原因打到埋点,按任务名聚合失败率;② 阈值告警:某个任务连续失败超过 N 次要主动告警,而不是等用户反馈;③ 手动补发入口:给运营一个后台能触发的"重试失败任务"按钮,兜住自动重试耗尽的情况。这三条做完,WorkManager 才算真正落地。
三条高频追问(续)
问:WorkManager 内部怎么做到持久化?
Room 数据库存 WorkSpec 表(类名、状态、参数、约束、尝试次数、时间),WorkManager 的调度器用 AlarmManager/JobScheduler 注册,进程被杀后由 SystemJobService 或闹钟回调重新拉起,队列不丢。这也解释了为什么它能"保证完成"——不是靠进程活着,而是靠持久化的规格 + 系统的重新调度。
问:周期性任务的周期能改吗?
不能低于 15 分钟(PeriodicWorkRequest 的构造参数限制)。这也是为什么"高频轮询"不适合它。另外周期任务的 setProgress 意义有限,因为执行时间短且不精确;真正的进度反馈要靠 setForegroundAsync 变成前台通知。
问:Constraints 里哪几条最容易被忽略?
setRequiresStorageNotLow(空间不足不跑)、setRequiresBatteryNotLow(低电量不跑)、setRequiresCharging(充电才跑)、setRequiresDeviceIdle(仅省电模式下的 API 23+)。"存储空间充足"这一条几乎总被忘记,而它恰恰是很多"上传失败"事故的根因。
落到项目里怎么做
一条能直接落地的规则:所有"最终要成功"的动作都补一个 WorkManager 补偿任务。比如即时上传失败后入队一个带重试的上传任务,运营的"手动同步"按钮背后也走同一条队列——这样链路上只有一份逻辑。
配套实践三条:① 统一封装 WorkScheduler.submit(name, request, policy),把 ExistingWorkPolicy 与标签规范固化;② 任务失败率上报埋点,超过阈值告警,避免"静默失败";③ 任务 Data 的 key 与 payload 集中定义成常量,避免跨进程或多 Worker 升级时参数不兼容。
给正在准备面试的你
WorkManager 这题最容易答成 API 清单,答好要落到三个机制 + 一张选型表。推荐这条线:① 三个核心能力(持久队列 / Constraints / WorkSpec 链)→ ② 为什么持久(Room + 系统调度器,所以"进程死了也不丢")→ ③ 选型判据(需要用户感知且立即执行 vs 可延迟可保证完成),并把心跳这类反例点出来 → ④ 追问层(retry vs failure、getItemId… 改成 Worker 分发、周期不能低于 15 分钟、setRequiresStorageNotLow 容易被忽略)。补上 ListenableWorker 处理回调式 SDK 这个细节,答案就显出实际经验了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:前台服务与后台限制:通知与省电的平衡
下一篇预告:BroadcastReceiver:静态注册与动态注册的存亡
有任何问题欢迎在评论区留言交流。