一、概念
用于简化在Android应用程序中执行后台任务的管理。它提供了一种灵活、可靠的方式来调度和执行异步任务,而无需开发人员过多关注任务的管理和设备状态。
很多安卓厂商为了保证电池的续航,所以当应用被退出后,任务的执行可能不是很及时甚至不会执行。 任务有10分钟限制,长时间任务还是用前台Service。适用于数据同步、日志上传、条件出发的后台任务。
1.1 特点
| 约束条件 | 可以指定任务执行的条件,例如在设备采用不按流量计费的网络连接时、当设备处于空闲状态或者有足够的电量时运行等,有助于减少不必要的任务执行、提高电池寿命和性能。 |
| 灵活性 | 可以创建单次或重复性任务,包括延迟执行、周期性执行和根据触发条件执行的任务。还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。 |
| 任务链 | 将多个任务串联起来,控制哪部分顺序运行,哪些部分并行运行。这对于处理复杂的工作流非常有用。 |
| 可靠性 | 确保任务完成而不会丢失,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。 |
| 兼容性 | 针对不同 Android 版本使用不同的后台任务调度API。最低支持 Android4(API14)。 |
1.2 使用场景
| 适用场景 | 延迟任务 | 任务不需要立即运行,可以推迟到合适的时机(充电、空闲、网络)。例如:定期同步数据、批量日志上传、夜间备份。 |
| 约束条件 | 任务需要满足特定条件才执行。例如:只在有 Wi-Fi 时上传大文件、设备充电时执行耗电操作。 | |
| 周期性任务 | 需要按固定间隔重复执行的任务(支持最小间隔 15 分钟)。例如:每24小时检查应用更新。 | |
| 任务链 | 任务有依赖关系或需要按顺序执行。例如:先下载数据,再处理数据,最后上传结果。 | |
| 不适用场景 | 需要立即执行的任务:实时响应用户操作。 | |
| 高优先级后台任务:音乐播放、更新定位(使用前台Service)。 | ||
| 短时快速任务:使用协程更轻量。 | ||
| 与生命周期相关任务:使用 lifecycle 组件。 | ||
二、添加依赖
implementation "androidx.work:work-runtime-ktx:2.10.1"
三、创建任务 CoroutineWorker
Java环境继承 Work,协程环境继承 CoroutineWorker。重写 doWork() 用于耗时任务,需要返回一个 Result 用于将任务结果通知给 WorkManager:success成功、failure失败、retry重试。
class MyWork(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
//该方法提供协程环境
override suspend fun doWork(): Result {
//耗时任务
return Result.success()
}
}
四、配置任务 WorkRequest
配置任务的触发条件:约束(链接WiFi、充电等)、运行周期(一次性、重复性)、延迟、重试策略。
4.1 一次性 / 重复性
创建 WorkRequest,一次性任务使用 OneTimeWorkRequest、重复性任务使用 PeriodicWorkRequest(间隔≥15分钟)。
4.1.1 单次任务 OneTimeWorkRequest

单次任务的初始状态为 ENQUEUED,任务会在满足其 Constraints 和初始延迟计时要求后立即运行。接下来,该任务会转为 RUNNING 状态,然后可能会根据任务的结果转为 SUCCEEDED、FAILED 状态;或者,如果结果是 retry,它可能会回到 ENQUEUED 状态。在此过程中,随时都可以取消任务,取消后任务将进入 CANCELLED 状态。
对于单次任务,若无需额外配置可使用静态方法 from() 创建,复杂任务使用 Build 构建器模式创建。
//无需额外配置(静态方法)
val myWorkRequest1 = OneTimeWorkRequest.from(MyWork::class.java)
//复杂任务(构建器模式)
val myWorkRequest2 = OneTimeWorkRequestBuilder<MyWork>()
//进行配置
.build()
4.1.2 重复任务 PeriodicWorkRequest

重复任务只有一个终止状态 CANCELLED。这是因为重复任务永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。

- repeatInterval 用于指定重复周期(但周期内的具体执行时机不确定,取决于设定的约束和系统调度),最小可设置的值为15分钟。
- flexTimeInterval 用于指定在重复周期中最后的某个时间段内执行,针对任务对时间敏感的需求(例如一小时的周期在最后15分钟期间执行),最小可设置的值为5分钟。
| PeriodicWorkRequestBuilder() | public inline fun <reified W : ListenableWorker> PeriodicWorkRequestBuilder( |
| public inline fun <reified W : ListenableWorker> PeriodicWorkRequestBuilder( repeatInterval: Duration, flexTimeInterval: Duration ): PeriodicWorkRequest.Builder |
//导包是java的Duration
val myWorkRequest = PeriodicWorkRequestBuilder<MyWork>(
Duration.ofHours(1), //一小时为周期
Duration.ofMinutes(15) //周期内最后15分钟执行
).build()
//重载版本
val myWorkRequest2 = PeriodicWorkRequestBuilder<MyWork>(
1, TimeUnit.HOURS,
15, TimeUnit.MINUTES
).build()
4.2 约束 Constraints
用于确保将任务延迟到满足最佳条件时运行。可设置多个约束,全部满足才会执行。
| setRequiredNetworkType() 网络 | fun setRequiredNetworkType(networkType: NetworkType) 设置在特定网络环境下才会执行。 NetworkType. NOT_REQUIRED 不需要网络 |
| setRequiresBatteryNotLow() 电量 | fun setRequiresBatteryNotLow(requiresBatteryNotLow: Boolean) 为 true 只有在设备电量充足时才会执行。 |
| setRequiresCharging() 充电 | fun setRequiresCharging(requiresCharging: Boolean) 为 true 只有在设备充电时才会执行。 |
| setRequiresDeviceIdle() 空闲 | fun setRequiresDeviceIdle(requiresDeviceIdle: Boolean) 为 true 只有在设备必须处于空闲状态才会执行。对批量操作非常有用,因为批量操作可能会降低设备商正在积极运行的应用性能。 |
| setRequiresStorageNotLow() 存储 | fun setRequiresStorageNotLow(requiresStorageNotLow: Boolean) 为 true 只有在存储空间充足时才会执行。 |
//创建约束
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
.build()
//设置给WorkRequest
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setConstraints(constraints)
.build()
4.3 延迟 setInitialDelay()
执行前进行一段时间的延迟。重复性任务只在首次执行会延迟。
| fun setInitialDelay(duration: Duration) |
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setInitialDelay(Duration.ofMinutes(15))
.build()
4.4 输入输出数据 setInputData()
输入输出数据都是Map形式的Data类型。
| fun setInputData(inputData: Data) |
class MyWork(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
//定义key传参和读取更安全
companion object {
const val URL = "url"
}
override suspend fun doWork(): Result {
val url = inputData.getString(URL) ?: return Result.failure()
val age = inputData.getInt("age", 0)
downloadPic(url)
return Result.success()
}
}
//方式一(参数2是Any?在Work中获取时不好把控)
val data1 = workDataOf(MyWork.URL to "http://...")
//方式二(推荐)
val data2 = Data.Builder()
.putString(MyWork.URL, "http://...") //推荐使用MyWork中定义好的key
.putInt("age", 18)
.build()
//通过WorkRequest输入
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setInputData(data2)
.build()
4.5 重试 setBackoffCriteria()
doWork() 中返回 Resulr.retry() 的时候代表任务需要重新执行,通过 setBackoffCriteria() 配置重试时间(最小值为10秒)和时间的增长策略。
| setBackoffCriteria() | fun setBackoffCriteria(backoffPolicy: BackoffPolicy, duration: Duration) 策略定义了后续再次重试时间的增长方式,假设时间设为10秒,BackoffPolicy.LINEAR 后续为 20、30、40,BackoffPolicy.EXPONENTIAL 后续为 20、40、80。 |
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
4.6 加急 setExpedited()
针对一些很重要或由用户启动的后台任务(付款订阅,发送消息)、需要立即执行并在几分钟内完成的简短任务、即便关闭应用也应继续执行。但在某些情况下会被延迟:
- 负载:系统负载过高,过多的任务已经在运行或系统内存不足时。
- 配额:应用在前台运行时加急不会被限制,在后台时系统为了有效在应用间平衡资源,每个应用都会获得执行时间配额(取决于待机模式存储分区和进程重要性),用完后在刷新前无法再加急。
| setExpedited() | fun setExpedited(policy: OutOfQuotaPolicy) 传入配额超出后的执行策略 OutOfQuotaPolicy .RUN_AS_NON_EXPEDITED_WORK_REQUEST 变成普通任务 .DROP_WORK_REQUEST 丢弃任务 |
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
4.6.1 兼容性适配(前台Service)
Android12 之前的版本加急会采用前台服务,因此需要构建一个通知(12之后的版本不会显示通知)。重写 getForegroundInfo() 来构建通知,在 doWork() 中,调用 setForeground() 将其传入。需要注意处理通知渠道、前台服务类型和服务权限声明。
class MyWork(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
private val channelId = "后台下载"
private val notificationId = 123456
override suspend fun doWork(): Result {
//设置前台服务
setForeground(getForegroundInfo())
return Result.success()
}
//重写提供通知(注意处理通知渠道、前台服务类型声明和前台服务权限申请)
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = NotificationCompat.Builder(APP.context, channelId)
.setContentText("后台下载中")
.build()
//参数三是前台服务类型
return ForegroundInfo(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
}
}
4.7 标记 addTag()
通过标记一组相关的任务,可以批量取消任务请求或获取状态。单个请求可添加多个标记。
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.addTag("download")
.addTag("important")
.build()
五、管理任务
5.1 普通提交、唯一性提交
将 WorkRequest 提交后,WorkManager 就会根据配置执行 doWork() 里的业务代码。注意避免重复提交,否则会执行多次。唯一性可以确保同一时刻只有一个具体特定名称的任务实例。uniqueName(唯一性名称)是由开发者指定仅绑定一个任务实例(ID是由WorkManager生成,Tag能绑定多个实例)。
| 普通提交 | fun enqueue(request: WorkRequest): Operation 单次、重复任务都可提交。 |
| 唯一性提交 | fun enqueueUniqueWork( 适用于提交单次任务。 |
| enqueueUniquePeriodicWork( 适用于提交重复任务。 |
| ExistingWorkPolicy | REPLACE:新任务替换当前任务,当前任务会被取消。 KEEP:忽略新任务,保留当前任务。 APPEND:将新任务链接到当前任务尾部,当前任务完成后执行新任务,当前任务是新任务的先决条件,如果当前任务变为 CANCELLED 或 FAILED 状态,新任务也会变为相同状态且不会执行。若想新任务能得到执行用下面的。 APPEND_AND_REPLACE:同上,使用该策略无论当前任务的状态如何都执行新任务。 |
| ExistingPeriodicWorkPolicy | KEEP:忽略新任务,保留当前任务。 UPDATE:下一个周期采用新任务。 CANCEL_AND_REENQUEUE:新任务替换当前任务,当前任务会被取消。 |
WorkManager.getInstance(context)
.enqueue(myWorkRequest)
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork("", ExistingPeriodicWorkPolicy.UPDATE, myWorkRequest)
5.2 观察 WorkInfo
任务提交后可能需要根据其执行情况做相应的处理(状态查询、获取进度、获取结果、取消任务),可以通过 id、tag、uniqueName 获取到任务对应的 WorkInfo,进而获取 State 以及 Result.success() 携带的结果。
5.2.1 简单查询 getWorkInfoBy***()
| fun getWorkInfoById(id: UUID): ListenableFuture<WorkInfo?> fun getWorkInfoByIdFlow(id: UUID): Flow<WorkInfo?> 通过 id 查询。 |
| fun getWorkInfosByTag(tag: String): ListenableFuture<List<WorkInfo>> fun getWorkInfosByTagLiveData(tag: String): LiveData<List<WorkInfo>> fun getWorkInfosByTagFlow(tag: String): Flow<List<WorkInfo>> 通过 tag 查询。 |
| fun getWorkInfosForUniqueWork(uniqueWorkName: String): ListenableFuture<List<WorkInfo>> fun getWorkInfosForUniqueWorkLiveData(uniqueWorkName: String): LiveData<List<WorkInfo>> fun getWorkInfosForUniqueWorkFlow(uniqueWorkName: String): Flow<List<WorkInfo>> 通过 uniqueName 查询。 |
WorkManager
.getInstance(context)
.getWorkInfoByIdLiveData(myWorkRequest.id)
.observe(lifecyclerOwner) { workInfo ->
when (workInfo?.state) {
WorkInfo.State.ENQUEUED -> TODO()
WorkInfo.State.RUNNING -> TODO()
WorkInfo.State.SUCCEEDED -> TODO()
WorkInfo.State.FAILED -> TODO()
WorkInfo.State.BLOCKED -> TODO()
WorkInfo.State.CANCELLED -> TODO()
null -> TODO()
}
}
5.2.2 复杂查询 WorkQuery
支持按照任务的 id、tag、uniqueName、state进行组合查询。
| WorkQuery | fun fromIds(ids: List<UUID>): Builder fun fromTags(tags: List<String>): Builder fun fromUniqueWorkNames(uniqueWorkNames: List<String>): Builder fun fromStates(states: List<WorkInfo.State>): Builder 先通过不同的方式找到对应的任务。 |
| fun addIds(ids: List<UUID>): Builder 再添加其它附加条件。 | |
| WorkManager | fun getWorkInfos(workQuery: WorkQuery): ListenableFuture<List<WorkInfo>> fun getWorkInfosLiveData(workQuery: WorkQuery): LiveData<List<WorkInfo>> 通过 WorkQuery 进行复杂查询。 |
//查找带有“syncTag”标签,处于SUCCESS状态,且唯一名称为“preProcess”或“sync”的所有任务
val workQuery = WorkQuery.Builder
//先通过不同的方式找到对应的任务
.fromTags(listOf("syncTag"))
//再添加其它附加条件
.addStates(listOf(WorkInfo.State.SUCCEEDED))
.addUniqueWorkNames(listOf("preProcess", "sync")
.build()
WorkManager
.getInstance(APP.context)
.getWorkInfos(workQuery)
5.2.3 获取状态
WorkManager
.getInstance(context)
.getWorkInfoByIdLiveData(myWorkRequest.id)
.observe(lifecyclerOwner) { workInfo ->
when (workInfo?.state) {
WorkInfo.State.ENQUEUED -> TODO()
WorkInfo.State.RUNNING -> TODO()
WorkInfo.State.SUCCEEDED -> TODO()
WorkInfo.State.FAILED -> TODO()
WorkInfo.State.BLOCKED -> TODO()
WorkInfo.State.CANCELLED -> TODO()
null -> TODO()
}
}
5.2.4 设置和获取进度
class MyWork(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
companion object {
const val PROGRESS = "progress"
}
override suspend fun doWork(): Result {
val firstProgress = workDataOf(PROGRESS to 0)
val lastProgress = workDataOf(PROGRESS to 100)
setProgress(firstProgress)
delay(1000)
setProgress(lastProgress)
return Result.success()
}
}
WorkManager
.getInstance(APP.context)
.getWorkInfoByIdLiveData(workRequset.id)
.observe(livfcycleOwner) { workInfo ->
workInfo?.let {
val progress = it.progress.getInt(MyWork.PROGRESS, 0)
if (progress == 0) {}
if (progress == 100) {}
}
}
5.3 取消
当不再需要运行先前提交的任务时,可以通过 id、tag、uniqueName 进行取消,或全部取消。如果任务已经完成则不会执行任何操作,否则任务的状态会变成 CANCELLED 之后就不会再执行这个任务,其它依赖此任务的任务也会变为 CANCELLED。任务会因为以下几种原因而停止:
- 你明确要求取消它( 例如通过调用 WorkManager.cancelWorkbyId() 取消)。
- 对于唯一性工作,明确的将 ExistingWorkPolicy = REPLACE 的新任务提交,当前的任务会被立即视为已取消。
- 任务的约束条件已不再满足。
- 系统处于某种原因指示你的应用停止工作。如果超过10分钟的执行期限,可能会发生这种情况。该任务会调度为在稍后重试。
| fun cancelWorkById(id: UUID): Operation 通过 id 取消。 fun cancelAllWorkByTag(tag: String): Operation 通过 tag 取消。 fun cancelUniqueWork(uniqueWorkName: String): Operation 通过 uniqueName 取消。 fun cancelAllWork(): Operation 全部取消。 |
5.4 更新
5.4.1 使用 updateWork()
| 避免取消任务 | 通常应避免取消现有任务再将新任务提交,可能会导致重复执行某些任务和额外编写大量代码。(计算期间被取消,新任务需要重新计算。取消重复任务,新任务需要计算时间偏移来保证执行时间的一致性) |
| 应该取消的任务 | 一次性任务和重复性任务不能通过更新去替换。 |
| 适合更新的场景 | 约束 |
获取现有任务的 id,创建新的 WorkRequest,设置约束 Constraints,WorkManager 调用 updateWork() 将新的任务传递。
val oldWorkRequset = OneTimeWorkRequestBuilder<MyWork>().build()
val workManager = WorkManager.getInstance(APP.context)
workManager.enqueue(oldWorkRequset)
//拿到旧任务的id
//不能直接通过旧WorkRequest拿到,就通过uniqueName或tag拿到WorkInfo再拿到id
val oldWorkId = oldWorkRequset.id
//创建新的约束
val newConstraints = Constraints.Builder()
.setRequiresCharging(false)
.build()
//创建新的任务
val newRequest = OneTimeWorkRequestBuilder<MyWork>()
.setConstraints(newConstraints)
.setId(oldWorkId) //传入旧的任务id
.build()
//更新任务(每次更新都会将 generation + 1)
workManager.updateWork(newRequest)
//更新后,通过新旧WorkInfo获取的ID、generation相同
val oldWorkInfo = workManager.getWorkInfoById(oldWorkRequset.id).get()
val newWorkInfo = workManager.getWorkInfoById(newWorkRequest.id).get()
//都是7ae9519a-dc70-4a04-84fc-e91900d50c11
Log.e("oldWorkInfo", oldWorkInfo?.id.toString())
Log.e("newWorkInfo", newWorkInfo?.id.toString())
//都是1
Log.e("oldWorkInfo", oldWorkInfo?.generation.toString())
Log.e("newWorkInfo", newWorkInfo?.generation.toString())
5.4.2 使用 ExistingPeriodicWorkPolicy.UPDATE
六、任务链
当需要以特定顺序运行多个单次任务时,可以串接单个任务(顺序执行)或任务集合(集合中的任务并行执行)。前一个节点执行成功才会继续往下执行。

如果节点失败后续节点不会被执行。若配置了重试策略,失败的任务会重试,不会影响与它并行执行(同级)的节点。

若未配置重试策略或重试已用尽,后续任务不会执行并被标记为 FAILED。

若有节点被取消,后续的任务都会被标记为 CANCELLED。

向 FAILED 或 CANCELLED 的任务链追加提交任务,新的任务也会被同样标记,如果想新任务能被执行,将 ExistingWorkPolicy 配置为 APPEND_OR_REPLACE。
6.1 基本用法
通过 beginWith() 创建首个节点,通过 then() 追加节点,可传入单个任务或任务集合,集合中的任务会并行执行。
val requset1 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset2 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset3 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset4 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset5 = OneTimeWorkRequestBuilder<MyWork>().build()
WorkManager
.getInstance(APP.context)
.beginWith(requset1)
.then(listOf(requset2, requset3, requset4)) //这三个会并行运行
.then(requset5)
.enqueue()
6.1 处理输入冲突
前一个节点的输出结果会被当做后一个节点的输入参数。如果前一个节点是任务集合,通过任务的变量名区分结果,若存在相同的变量名,默认后执行完的会覆盖掉前一个的值,若想保留两个值,后一个节点的任务需要通过 setInputMerger() 配置不同的输入冲突策略。
| OverwritingInputMerger(默认) | 变量名相同时,后执行完的那个会覆盖掉前一个。 |
| ArrayCreatingInputMerger | 变量名相同时,通过数组保存每个值。 |
val requset5 = OneTimeWorkRequestBuilder<MyWork>()
.setInputMerger(ArrayCreatingInputMerger::class) //配置输入冲突策略
.build()
WorkManager
.getInstance(APP.context)
.beginWith(listOf(requset1, requset1, requset2)) //集合中有两个名为 request1 的任务
.then(requset5) //已配置
.enqueue()
6.1.1 OverwritingInputMerger(默认)

任务集合中有两个变量名是 “plantName1” 的任务,由于是并行执行,后执行完的会覆盖先执行完的。(图示假设第二个“plantName1”后执行完)
6.1.2 ArrayCreatingInputMerger

相同变量名的任务,值会通过数组保存。
七、延迟初始化
默认在启动应用时 WorkManager 就会初始化,根据合并规则在 app 下的 AndroidManifest 中重写来覆盖。
<!-- 如果应用不需要InitializationProvider的话可以直接移除InitializationProvider -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
<!--如果还用到其他Initializer,只是禁用WorkManagerInitializer的话-->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
八、自定义配置
让自定义的 Application 类实现 Configuration.Provider 接口,并提供自己的 Configuration.Provider.getWorkManagerConfiguration() 实现。当需要使用 WorkManager 时,请务必调用方法 WorkManager.getInstance(Context)。WorkManager 会调用应用的自定义 getWorkManagerConfiguration() 方法来发现其 Configuration(无需自行调用 WorkManager.initialize())
class MyApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.setExecutor(Executors.newFixedThreadPool(8))
.build()
}

322

被折叠的 条评论
为什么被折叠?



