目录
  1. 1. 一、为什么需要 WorkManager
    1. 1.1. 1.1 Android 后台任务调度演进史
    2. 1.2. 1.2 WorkManager 的设计哲学
  2. 2. 二、核心组件详解
    1. 2.1. 2.1 Worker —— 任务的执行单元
    2. 2.2. 2.2 WorkRequest —— 任务描述
    3. 2.3. 2.3 WorkRequest 的三种唯一性策略
  3. 3. 三、约束条件(Constraints)
    1. 3.1. 3.1 完整约束类型
    2. 3.2. 3.2 约束满足与调度时序
  4. 4. 四、任务链式调度
    1. 4.1. 4.1 顺序链
    2. 4.2. 4.2 复杂链(并联 + 串联)
    3. 4.3. 4.3 Worker 间数据传递
  5. 5. 五、任务状态机与观察
    1. 5.1. 5.1 WorkManager 的状态模型
    2. 5.2. 5.2 观察任务状态
    3. 5.3. 5.3 进度更新
  6. 6. 六、ExpeditedWork 与前台执行
    1. 6.1. 6.1 为什么需要 ExpeditedWork
    2. 6.2. 6.2 前台 Service 关联
  7. 7. 七、底层调度机制深入
    1. 7.1. 7.1 调度器选择逻辑
    2. 7.2. 7.2 任务持久化机制
    3. 7.3. 7.3 退避策略(Backoff Policy)
  8. 8. 八、测试
    1. 8.1. 8.1 使用 TestListenableWorkerBuilder 测试 Worker
    2. 8.2. 8.2 使用 TestDriver 控制延迟
  9. 9. 九、WorkManager vs 其他调度方案
  10. 10. 面试常考问题
JetPack全家桶(七)之WorkManager任务调度

WorkManager 是 Jetpack 中用于处理可延迟但必须执行的后台任务的调度库。它能保证任务在所有场景下被执行——即使应用退出、设备重启。WorkManager 内部根据 API 级别智能选择底层调度器。

一、为什么需要 WorkManager

1.1 Android 后台任务调度演进史

在 WorkManager 诞生之前,Android 开发者面对后台任务调度时需要根据不同的 API 级别选择不同的 API:

API 级别 可用调度 API 局限性
所有 Thread / Handler / AsyncTask(已废弃) 进程被杀后任务丢失
所有 Service + startForeground() 前台 Service 需要常驻通知;后台 Service (API 26+ 受限)
所有 AlarmManager 精确闹钟受 Doze 模式限制;API 19+ 非精确闹钟
API 9+ BroadcastReceiver + AlarmManager 需要在 Manifest 注册,管理复杂
API 21+ JobScheduler API 21+ 才可用;API < 21 需要兼容方案
API 14+(Firebase) FirebaseJobDispatcher 依赖 Google Play Services,国内设备不可用

这造成了严重的碎片化 —— 开发者需要为同一功能编写多套实现:

// 在没有 WorkManager 的年代,一个"定时同步"功能需要这样写:
fun scheduleSync(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val jobInfo = JobInfo.Builder(1, ComponentName(context, SyncJobService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.setPeriodic(TimeUnit.HOURS.toMillis(6))
.build()
jobScheduler.schedule(jobInfo)
} else {
// API < 21:用 AlarmManager + BroadcastReceiver 模拟
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, SyncReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
alarmManager.setInexactRepeating(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + TimeUnit.HOURS.toMillis(6),
TimeUnit.HOURS.toMillis(6),
pendingIntent
)
}
}

WorkManager 的出现统一了这一切:开发者只需用一套 API 定义任务,WorkManager 内部根据设备 API 级别和 Google Play Services 可用性自动选择最优调度器。

1.2 WorkManager 的设计哲学

WorkManager 解决的核心问题是 Android 上可延迟但必须执行的工作(deferrable guaranteed work):

  • 可延迟:工作不需要立即执行,可以在满足约束条件后在后台运行。不是 UI 相关的即时操作。
  • 必须执行:即使应用退出、设备重启,任务也必须在某个时刻完成。JobScheduler 在 API 21-22 的设备上会遇到任务丢失问题,WorkManager 内部有持久化机制保证不丢失。
  • 约束驱动:任务只在满足条件时才执行(有网络、电量充足、设备空闲等),避免在不合适的时间点消耗资源。

WorkManager 不适合的场景:

  • 需要精确时间触发的任务 → 使用 AlarmManager(如闹钟应用)
  • 需要立即执行且不需要持久化的任务 → 使用 CoroutineScope + Dispatchers
  • 需要前台执行的长时任务 → 使用前台 Service 或 WorkManager 的 setForeground()ExpeditedWork

二、核心组件详解

2.1 Worker —— 任务的执行单元

Worker 是实际执行任务的类。通常使用 CoroutineWorker(基于 Kotlin 协程):

/**
* 继承 CoroutineWorker,实现 doWork()
* CoroutineWorker 的 doWork() 是 suspend 函数,运行在 Dispatchers.Default
*/
class UploadLogWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
// 从 inputData 中获取输入参数
val logPath = inputData.getString("log_path")
?: return Result.failure(
workDataOf("error" to "log_path is missing")
)

val maxRetries = inputData.getInt("max_retries", 3)

return try {
// 模拟日志上传
val uploadedSize = uploadLogFile(logPath)

// 返回成功结果,携带输出数据
Result.success(
workDataOf(
"uploaded_size" to uploadedSize,
"uploaded_at" to System.currentTimeMillis()
)
)
} catch (e: IOException) {
// 根据重试次数决定是 retry 还是 failure
if (runAttemptCount < maxRetries) {
Result.retry()
} else {
Result.failure(
workDataOf(
"error" to e.message ?: "Unknown IO error",
"attempt_count" to runAttemptCount
)
)
}
} catch (e: Exception) {
// 非 IO 错误直接失败
Result.failure(
workDataOf("error" to (e.message ?: "Unknown error"))
)
}
}

/**
* 上传日志文件,返回上传字节数
* 这应该是具体的业务逻辑
*/
private suspend fun uploadLogFile(path: String): Long {
// 实际的网络上传逻辑
val file = File(path)
if (!file.exists()) {
throw IOException("Log file not found: $path")
}
// ... 执行上传
return file.length()
}
}

CoroutineWorker 的线程模型:

  • doWork() 默认运行在 Dispatchers.Default 上(CPU 密集型线程池)
  • 如需切换到 Dispatchers.IO(网络/文件操作),使用 withContext(Dispatchers.IO) { ... }
  • 切勿在 doWork() 中直接操作 UI 组件 —— WorkManager 的运行环境可能没有 UI 线程

Worker 的两种实现方式:

基类 线程 协程支持 推荐度
Worker 后台线程(Executor 线程池) 不支持(需手动用 runBlockingListenableFuture 仅 Java 项目使用
CoroutineWorker Dispatchers.Default 原生 suspend 支持 推荐(Kotlin 项目首选)
ListenableWorker 自定义 自定义 需要与 RxJava/ListenableFuture 集成时使用

2.2 WorkRequest —— 任务描述

WorkRequest 描述了”执行什么 Worker、何时执行、有哪些约束”。

OneTimeWorkRequest —— 一次性任务:

// 最简单的一次性任务
val uploadWork = OneTimeWorkRequestBuilder<UploadLogWorker>()
.build()

// 带完整配置的一次性任务
val uploadWorkFull = OneTimeWorkRequestBuilder<UploadLogWorker>()
// 输入数据
.setInputData(
workDataOf(
"log_path" to "/data/data/com.example/files/logs/app.log",
"max_retries" to 5,
"compress_first" to true
)
)
// 约束条件(见下一节)
.setConstraints(constraints)
// 延迟执行(最少 10 秒后执行)
.setInitialDelay(10, TimeUnit.SECONDS)
// 退避策略(重试时的延迟策略)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, // 指数退避
WorkRequest.MIN_BACKOFF_MILLIS, // 初始延迟 10 秒
TimeUnit.MILLISECONDS
)
// 为任务添加标签(方便批量管理)
.addTag("upload_log")
.addTag("periodic_sync")
.build()

// 提交任务
WorkManager.getInstance(context).enqueue(uploadWorkFull)

PeriodicWorkRequest —— 周期任务:

// 每隔 6 小时执行一次的周期任务
val periodicSyncWork = PeriodicWorkRequestBuilder<SyncWorker>(
6, TimeUnit.HOURS, // 周期(最小 15 分钟)
15, TimeUnit.MINUTES // flexInterval:在周期末尾的 15 分钟内弹性执行
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true) // 尽量在充电时执行
.build()
)
.addTag("periodic_sync")
.build()

WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"daily_sync", // 唯一名称
ExistingPeriodicWorkPolicy.UPDATE, // 如果已存在则更新策略
periodicSyncWork
)

flexInterval 解释:

PeriodicWork: repeatInterval = 6 hours, flexInterval = 15 minutes

时间轴:
[ 0s ............................ 5h45m ][ 15m ][ 6h00m ................]
↑________↑
flex window(弹性窗口)
任务在这 15 分钟内执行均满足要求

设置 flexInterval 允许 WorkManager 在满足约束条件时弹性安排任务执行时间,而非严格按固定时间触发。这有助于系统进行批量调度(batch),节省电量。

2.3 WorkRequest 的三种唯一性策略

当多次提交同名任务时,通过 ExistingWorkPolicy 控制行为:

// REPLACE:取消已存在的同名任务,用新任务替换
WorkManager.getInstance(context)
.enqueueUniqueWork(
"upload_photos",
ExistingWorkPolicy.REPLACE,
uploadWork
)

// KEEP:如果同名任务已存在,保留旧任务,忽略新任务
WorkManager.getInstance(context)
.enqueueUniqueWork(
"upload_photos",
ExistingWorkPolicy.KEEP,
uploadWork
)

// APPEND:将新任务追加到同名任务链末尾,等旧任务完成后再执行
WorkManager.getInstance(context)
.enqueueUniqueWork(
"upload_photos",
ExistingWorkPolicy.APPEND,
uploadWork
)

// 对于周期性任务,也有对应的策略:
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"daily_sync",
ExistingPeriodicWorkPolicy.UPDATE, // 用新策略更新已有的周期任务
periodicWork
)
// 或 ExistingPeriodicWorkPolicy.KEEP // 保留已有的,忽略新的

三、约束条件(Constraints)

3.1 完整约束类型

val constraints = Constraints.Builder()
// 网络类型
.setRequiredNetworkType(NetworkType.CONNECTED)
// NetworkType 可选值:
// NOT_REQUIRED - 无需网络
// CONNECTED - 需要任何网络连接(WiFi 或蜂窝)
// UNMETERED - 需要非计费网络(WiFi)
// NOT_ROAMING - 需要非漫游网络
// METERED - 需要计费网络(蜂窝网络)

// 电池限制
.setRequiresBatteryNotLow(true) // 电量非低时(>15% 或省电模式未激活)

// 充电状态
.setRequiresCharging(true) // 在充电中

// 设备空闲
.setRequiresDeviceIdle(true) // API 23+:设备处于 Doze 空闲状态

// 存储空间
.setRequiresStorageNotLow(true) // 存储空间非低

// (已废弃)内容 URI 触发器
// .addContentUriTrigger(uri, true) // API 24+:指定 URI 变更时触发

.build()

约束组合策略:

// 场景 1:数据同步任务——WiFi 下执行
val syncConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // 仅 WiFi
.setRequiresBatteryNotLow(true) // 电量不低于阈值
.build()

// 场景 2:日志上报任务——任何网络下执行,但避免影响用户体验
val logConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 有网即可
.setRequiresDeviceIdle(true) // 等设备空闲时
.build()

// 场景 3:缓存预热——仅在充电+WiFi 时执行
val prefetchConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi
.setRequiresCharging(true) // 充电中
.build()

3.2 约束满足与调度时序

WorkManager 不会轮询检查约束是否满足。约束满足检测依赖于系统的广播事件:

网络状态变化 → ConnectivityManager 回调 → WorkManager 检查 pending 任务
充电状态变化 → BatteryManager 广播 → WorkManager 检查 pending 任务
设备空闲 → DeviceIdleMode 回调 → WorkManager 检查 pending 任务

这意味着:如果一个任务因为”需要 WiFi 网络”而被阻塞,当用户连接到 WiFi 时,WorkManager 会在收到系统回调后立即(少数秒内)执行该任务,而不是等到下一个周期才检查。

四、任务链式调度

4.1 顺序链

val downloadWork = OneTimeWorkRequestBuilder<DownloadWorker>().build()
val verifyWork = OneTimeWorkRequestBuilder<VerifyWorker>().build()
val decryptWork = OneTimeWorkRequestBuilder<DecryptWorker>().build()
val processWork = OneTimeWorkRequestBuilder<ProcessWorker>().build()

// 线性链:下载 → 校验 → 解密 → 处理
WorkManager.getInstance(context)
.beginWith(downloadWork)
.then(verifyWork)
.then(decryptWork)
.then(processWork)
.enqueue()

4.2 复杂链(并联 + 串联)

// 同时下载多个文件,完成后统一处理
val downloadA = OneTimeWorkRequestBuilder<DownloadWorkerA>().build()
val downloadB = OneTimeWorkRequestBuilder<DownloadWorkerB>().build()
val downloadC = OneTimeWorkRequestBuilder<DownloadWorkerC>().build()

val mergeWork = OneTimeWorkRequestBuilder<MergeWorker>().build()
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>().build()

WorkManager.getInstance(context)
.beginWith(listOf(downloadA, downloadB, downloadC)) // 并行下载
.then(mergeWork) // 下载全部完成后合并
.then(uploadWork) // 合并完成后上传
.enqueue()

4.3 Worker 间数据传递

WorkManager 通过 Data 对象传递数据:

// Worker A:输出数据
class GenerateReportWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val reportPath = generateReport()
return Result.success(
workDataOf(
"report_path" to reportPath,
"report_size" to File(reportPath).length(),
"generated_at" to System.currentTimeMillis()
)
)
}
}

// Worker B:读取 Worker A 的输出
class UploadReportWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 从 inputData 读取前一个 Worker 的输出
val reportPath = inputData.getString("report_path") ?:
return Result.failure()

val reportSize = inputData.getLong("report_size", 0L)

// 执行上传
uploadFile(reportPath)

return Result.success(
workDataOf("uploaded_at" to System.currentTimeMillis())
)
}
}

// 链式执行,数据自动传递
WorkManager.getInstance(context)
.beginWith(reportWork)
.then(uploadWork)
.enqueue()

Data 的限制:

  • 最大 10KB(内部使用 Bundle 序列化,受 Binder 事务限制)
  • 支持的类型:IntLongFloatDoubleBooleanStringStringArray
  • 大数据应在 Worker 内部通过文件路径或数据库传递,而非直接放入 Data

五、任务状态机与观察

5.1 WorkManager 的状态模型

每个 WorkRequest 都有一个生命周期,经历以下状态流转:

             ┌──────────┐
│ BLOCKED │ ← 有前置任务未完成,或约束条件未满足
└────┬─────┘
│ 前置任务完成 + 约束满足

┌───────────┐
│ ENQUEUED │ ← 任务已入队,等待执行
└─────┬─────┘
│ 轮到该任务执行

┌───────────┐
│ RUNNING │ ← 正在执行 doWork()
└─┬─────┬───┘
│ │
┌────────┘ └────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ SUCCEEDED │ │ FAILED │ ← doWork() 返回 Result.failure()
└──────────┘ └──────────┘

┌──────┴──────┐
│ RETRY? │ ← doWork() 返回 Result.retry()
└──────┬──────┘
│ yes(未超过重试次数)
└────→ ENQUEUED

5.2 观察任务状态

// 方式 1:通过 ID 观察单个任务
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadWork.id)
.observe(lifecycleOwner) { workInfo ->
when (workInfo?.state) {
WorkInfo.State.ENQUEUED -> Log.d(TAG, "任务已入队")
WorkInfo.State.RUNNING -> {
val progress = workInfo.progress.getInt("percent", 0)
updateProgressBar(progress)
}
WorkInfo.State.SUCCEEDED -> {
val size = workInfo.outputData.getLong("uploaded_size", 0)
showUploadSuccess(size)
}
WorkInfo.State.FAILED -> {
val error = workInfo.outputData.getString("error") ?: "未知错误"
showUploadFailed(error)
}
WorkInfo.State.BLOCKED -> Log.d(TAG, "任务被阻塞")
WorkInfo.State.CANCELLED -> Log.d(TAG, "任务已取消")
}
}

// 方式 2:通过 Tag 观察一组任务
WorkManager.getInstance(context)
.getWorkInfosByTagLiveData("sync_group")
.observe(lifecycleOwner) { workInfoList ->
val allFinished = workInfoList.all { it.state.isFinished }
val anyFailed = workInfoList.any { it.state == WorkInfo.State.FAILED }
}

// 方式 3:通过唯一名称观察
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkLiveData("upload_photos")
.observe(lifecycleOwner) { workInfoList ->
workInfoList.forEach { workInfo ->
Log.d(TAG, "工作 ${workInfo.id}: ${workInfo.state}")
}
}

// 方式 4:Kotlin Flow 方式(推荐用于 ViewModel)
viewModelScope.launch {
WorkManager.getInstance(context)
.getWorkInfoByIdFlow(uploadWork.id)
.collect { workInfo ->
_uiState.value = when (workInfo?.state) {
WorkInfo.State.RUNNING -> UiState.Uploading
WorkInfo.State.SUCCEEDED -> UiState.Success
WorkInfo.State.FAILED -> UiState.Error
else -> UiState.Idle
}
}
}

5.3 进度更新

class ProgressWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
val totalImages = 100

for (i in 1..totalImages) {
// 模拟处理图片
processImage(i)

// 更新进度(0-100)
val progress = workDataOf(
"percent" to (i * 100 / totalImages),
"current" to i,
"total" to totalImages
)
setProgress(progress) // 关键:调用 setProgress() 推送进度

// 检查任务是否被取消
if (isStopped) {
return Result.failure()
}
}
return Result.success()
}
}

六、ExpeditedWork 与前台执行

6.1 为什么需要 ExpeditedWork

默认情况下,后台任务可能被系统延迟执行(Doze 模式、App Standby Bucket 等)。但有些任务需要尽快完成:

  • 用户主动触发的”立即同步”
  • 应用关闭前的状态保存
  • 关键的日志上报(崩溃恢复场景)

ExpeditedWork 在 Android 12+(API 31+)上使用系统级的加急作业(expedited job),在旧设备上使用前台 Service 作为回退方案。

val expeditedWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()

WorkManager.getInstance(context).enqueue(expeditedWork)

OutOfQuotaPolicy 选项:

  • RUN_AS_NON_EXPEDITED_WORK_REQUEST:当应用超出加急配额时,降级为普通任务
  • DROP_WORK_REQUEST:当超出配额时直接丢弃任务

6.2 前台 Service 关联

对于需要长时间运行且用户需要感知的任务(如文件下载),可以使用 setForeground()

class DownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
// 设置前台通知(Android 12+ 要求长时间任务必须前台化)
setForeground(
ForegroundInfo(
notificationId = 1001,
createDownloadNotification(), // 构建通知
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
)

return try {
downloadLargeFile { progress ->
// 更新通知进度条
setForeground(
ForegroundInfo(
1001,
createDownloadNotification(progress)
)
)
}
Result.success()
} catch (e: Exception) {
Result.failure()
}
}

private fun createDownloadNotification(progress: Int = 0): Notification {
// 构建带进度条的下载通知
return NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentTitle("正在下载")
.setContentText("已下载 $progress%")
.setProgress(100, progress, false)
.setOngoing(true)
.build()
}
}

七、底层调度机制深入

7.1 调度器选择逻辑

WorkManager 内部调度器的选择优先级如下:

1. 如果有 Google Play Services → 使用 GcmNetworkManager
(但实际上从 WorkManager 2.3+ 开始不再使用 GCM)
2. API 23+ → 使用 JobScheduler
3. API 14-22 → 使用 AlarmManager + BroadcastReceiver

内部实现代码位于 androidx.work.impl.background.systemjob.SystemJobScheduler
和 androidx.work.impl.background.systemalarm.SystemAlarmScheduler

7.2 任务持久化机制

WorkManager 将任务信息持久化到内部 Room 数据库中(默认数据库名 workmanager.db):

-- WorkManager 内部 Room 数据库的核心表结构(简化)
CREATE TABLE WorkSpec (
id TEXT PRIMARY KEY, -- WorkRequest 的唯一 ID
state INTEGER NOT NULL, -- 当前状态(ENQUEUED / RUNNING / SUCCEEDED / ...)
worker_class_name TEXT NOT NULL, -- Worker 类全限定名
input_data BLOB, -- 输入 Data(序列化)
output_data BLOB, -- 输出 Data(序列化)
initial_delay INTEGER NOT NULL, -- 初始延迟(毫秒)
interval_duration INTEGER NOT NULL, -- 周期(毫秒,0 表示一次性)
flex_duration INTEGER NOT NULL, -- 弹性窗口(毫秒)
run_attempt_count INTEGER NOT NULL, -- 已尝试次数
backoff_policy INTEGER NOT NULL, -- 退避策略
backoff_delay_duration INTEGER, -- 退避延迟(毫秒)
period_start_time INTEGER, -- 周期任务的起始时间
schedule_requested_at INTEGER, -- 调度请求时间
run_duration_nanos INTEGER, -- 已运行时长
constraints BLOB -- 约束条件(序列化)
);

当应用进程被杀或设备重启后,WorkManager 读取 WorkSpec 表,恢复所有未完成的任务。

7.3 退避策略(Backoff Policy)

当 Worker 返回 Result.retry() 时,WorkManager 按退避策略延迟重试:

// 线性退避:每次重试间隔固定递增
setBackoffCriteria(
BackoffPolicy.LINEAR,
WorkRequest.MIN_BACKOFF_MILLIS, // 初始 10 秒
TimeUnit.MILLISECONDS
)
// 重试间隔:10s, 20s, 30s, 40s, ...

// 指数退避(推荐):每次重试间隔指数增长
setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS, // 初始 10 秒
TimeUnit.MILLISECONDS
)
// 重试间隔:10s, 20s, 40s, 80s, ...

// 默认最大重试次数为 10(可通过 inputData 控制)

八、测试

8.1 使用 TestListenableWorkerBuilder 测试 Worker

@RunWith(AndroidJUnit4::class)
class UploadLogWorkerTest {

private lateinit var context: Context

@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
}

@Test
fun testUploadSuccess() = runTest {
// 通过 TestListenableWorkerBuilder 创建 Worker 实例
val worker = TestListenableWorkerBuilder<UploadLogWorker>(context)
.setInputData(
workDataOf(
"log_path" to "/test/path/log.txt",
"max_retries" to 3
)
)
.build()

// 执行 doWork() 并验证结果
val result = worker.doWork()
assertThat(result, `is`(ListenableWorker.Result.success()))
}

@Test
fun testUploadFailure_exhaustedRetries() = runTest {
val worker = TestListenableWorkerBuilder<UploadLogWorker>(context)
.setInputData(
workDataOf("log_path" to "/nonexistent/file.log")
)
.setRunAttemptCount(5) // 模拟已重试 5 次
.build()

val result = worker.doWork()
assertThat(result, instanceOf(ListenableWorker.Result.Failure::class.java))
}
}

8.2 使用 TestDriver 控制延迟

@RunWith(AndroidJUnit4::class)
class WorkManagerTest {

private lateinit var workManager: WorkManager
private lateinit var testDriver: TestDriver

@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()

// 配置 WorkManager 使用测试模式
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor()) // 同步执行器
.build()

WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
workManager = WorkManager.getInstance(context)
testDriver = WorkManagerTestInitHelper.getTestDriver(context)!!
}

@Test
fun testDelayedWork() = runTest {
val work = OneTimeWorkRequestBuilder<TestWorker>()
.setInitialDelay(10, TimeUnit.MINUTES)
.build()

workManager.enqueue(work).result.get()

// TestDriver 允许我们在测试中控制时间
// 快进 10 分钟
testDriver.setInitialDelayMet(work.id)
// 验证约束满足后任务执行
testDriver.setAllConstraintsMet(work.id)

val workInfo = workManager.getWorkInfoById(work.id).get()
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
}
}

九、WorkManager vs 其他调度方案

方案 精确时间 保证执行 系统约束 持久化 适用场景
WorkManager 支持 大部分后台任务
JobScheduler 有限保证 支持 有限 API 21+,WorkManager 底层
AlarmManager 是(受限) 闹钟/提醒/精确定时
DownloadManager 网络 大文件下载
Foreground Service 立即 需通知 用户可感知的长时任务
CoroutineWorker 立即 应用内即时异步任务

面试常考问题

Q1:WorkManager 与 Service、JobScheduler、AlarmManager 的区别?

WorkManager 是最上层抽象封装,内部根据 API 级别自动路由:

  • API 23+ → JobScheduler
  • API 14-22 → AlarmManager + BroadcastReceiver

同时提供约束系统、链式调度、状态观察、任务持久化等高级功能。Service 适合前台任务,AlarmManager 适合精确定时(如闹钟),JobScheduler 适合条件触发的后台任务但仅限 API 21+ 且无持久化保证。

Q2:doWork() 在哪个线程执行?

WorkerdoWork() 默认在 WorkManager 的内部 Executor 线程池上执行(后台线程)。CoroutineWorkerdoWork()Dispatchers.Default 上运行。切勿在 doWork() 中直接操作 UI 组件。如需在主线程更新 UI,应用 LiveData/Observer 观察任务状态。

Q3:PeriodicWorkRequest 最小间隔为何是 15 分钟?

为了减少后台唤醒对电池的消耗,Android 强制 PeriodicWorkRequest 的最小间隔为 15 分钟(MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L)。这是 Android 系统级的限制,WorkManager 内部在 WorkSpec 中定义了该常量。如需更短间隔的后台任务,应使用带超时的前台 Service,或考虑 Handler.postDelayed() + AlarmManager(注意 Doze 限制)。

Q4:WorkManager 如何保证任务在设备重启后仍被执行?

WorkManager 将所有任务信息(WorkSpec)持久化到 Room 数据库中。设备启动后,WorkManager 在初始化时读取数据库中的待执行任务,重新注册到底层调度器(JobScheduler 或 AlarmManager)。这也是为什么 WorkManager 需要 android.permission.RECEIVE_BOOT_COMPLETED 权限 —— 如果使用了 AlarmManager 调度器(API < 23)。

Q5:如何取消多个任务?

  • 通过 id 取消:WorkManager.getInstance(context).cancelWorkById(workRequest.id)
  • 通过 tag 取消:WorkManager.getInstance(context).cancelAllWorkByTag("sync_group")
  • 通过唯一名称取消:WorkManager.getInstance(context).cancelUniqueWork("upload_photos")
  • 取消所有任务:WorkManager.getInstance(context).cancelAllWork()

取消是异步的 —— cancelWorkById() 返回的 ListenableFuture 完成后,任务状态才变为 CANCELLED。注意:已经在 RUNNING 状态的任务不会被强制中断,Worker 应定期检查 isStopped 来优雅退出。

Q6:WorkManager 如何处理 Doze 模式?

在 Doze 模式下,系统会延迟所有网络访问和 CPU 密集操作。WorkManager 通过以下策略处理 Doze:

  • 将任务委托给 JobScheduler(API 23+),后者原生理解 Doze 模式
  • 在 Doze 的维护窗口(Maintenance Window)内集中执行积压任务
  • 使用 WorkManager.periodicWorkRequest 时,任务可能在维护窗口内执行,而非严格按时触发
  • ExpeditedWork 可以在 Doze 模式下获得优先执行权(有配额限制)
打赏
  • 微信
  • 支付宝

评论