前言
通过上一篇 Android WorkManager 初探 的介绍,基本了解了 WorkManager 的特点,通过执行单次任务的示例,已经感受到了其强大之处。下面,了解一下如何使用 WorkManager 执行周期性任务。
周期性任务
在上一篇中我们了解到,WorkManager 可以基于设备当前的特性(包括网络、电量状态、存储空间等因素)约束任务执行的条件,同时还可以基于任务执行的结果进行设置不同的重试策略,下面我们就通过一个日志上传的任务来了解一下 WorkManager 更多的东西。
- 目标:日志上传,每一小时上传一次。只有在设备处于充电且 Wifi 连接时才可以执行。电量低时不允许执行,失败后需要根据指定的策略重试。
- 定义 Worker
kotlin
class UploadUserLogWork(appContext: Context, workerParameters: WorkerParameters) :
CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork(): Result {
val userId = inputData.getString(INPUT_TAG)
if (TextUtils.isEmpty(userId)) {
return Result.failure(Data.Builder().putString(OUTPUT_TAG, "userId is null").build())
}
val result = uploadLog(userId)
return if (result != null) {
val output = Data.Builder().putString(OUTPUT_TAG, result).build()
Result.success(output)
} else {
Result.retry()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
1, createNotification(
applicationContext, id, applicationContext.getString(R.string.app_name)
)
)
}
}
- 这里结合 Kotlin Coroutine 使用,因此定义 Worker 时需要继承 CoroutineWorker。除了实现
doWork
方法之外,还要实现 getForegroundInfo 方法,返回一个执行职务时的 Notification 即可。当然,如果你对 RxJava 更熟悉,也有相应的基类可以使用。 - 上传日志需要结合
userId
参数,参数为空时直接按失败处理。 - uploadLog 方法如果执行成功,按执行成功处理,否则返回 retry。
kotlin
private suspend fun uploadLog(userId: String?): String? {
return withContext(Dispatchers.IO) {
Log.i(TAG, "start upload")
delay(20000)
if (System.currentTimeMillis().toInt() % 1000 == 0) {
Log.i(TAG, "upload fail")
null
} else {
Log.i(TAG, "finish upload")
"${userId}_${System.currentTimeMillis()}"
}
}
}
这里简单模拟一个日志上传的任务,同时基于时间戳 mock 上传失败的场景,方便测试。
- 创建 WorkRequest
kotlin
fun createWorkRequest(userId: String?): PeriodicWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true)
.build()
return PeriodicWorkRequestBuilder<UploadUserLogWork>(1, TimeUnit.HOURS)
.setInputData(workDataOf(INPUT_TAG to userId))
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 3, TimeUnit.SECONDS)
.addTag(WORK_TAG)
.build()
}
- 首先定义约束条件,
NetworkType.UNMETERED
的意思是不按用量计费的网络,我们可以简单理解为 WiFi,非手机流量。其他的约束条件顾名思义。Constraints 的构造者模式还提供了其他的约束,比如设备是否处于空闲,存储容量是否过低等,我们基于实际情况做选择就好。当然,这些约束条件有些是互斥的,或者和重试策略是有冲突的,设置不恰当的话会在执行时抛出异常,毕竟万事不能既要又要还要。 - 通过
PeriodicWorkRequestBuilder
创建周期性的任务,这里是每一小时执行一次,需要注意的是最小间隔不能小于 15 分钟。即便你设置过小的时间,内部也会约束到 15 分钟。 - 将 userId 通过 InputData 传入,这个上一篇已经用过了。
setBackoffCriteria
是用来设置重试策略的,当 doWork 方法返回Result.retry
的时候,就会基于这里设置的策略进行重试。这里的机制是失败后首次延迟 3 秒重试,并且后续时间按线性增长,也就是 6,9,12 秒的时候执行。默认的增长策略是2的指数级增长。也就是会在 6,12,24 秒执行,这个基于实际场景做调整即可。这里需要注意的是,首次延迟重试的时间不得大于 10 秒,同时这个延迟的最大时间是 5 小时,不会无限制的增长。- 最后给 WorkRequest 添加 TAG,方便后续查询这个 WokerRequest 的信息及对其进行管理。
- 添加任务到队列
kotlin
fun triggerWork(application: Application) {
// userId = "123"
val request = createWorkRequest("123")
WorkManager.getInstance(application).enqueueUniquePeriodicWork(
WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, request
)
}
enqueueUniquePeriodicWork
方法接受 3 个参数:- uniqueWorkName - 用于唯一标识工作请求的 String。
- existingWorkPolicy - 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作,这个枚举有 4 个值。
- REPLACE:用新工作替换现有工作。此选项将取消现有工作。
- KEEP:保留现有工作,并忽略新工作。
- APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。现有工作将成为新工作的先决条件。如果现有工作变为 CANCELLED 或 FAILED 状态,新工作也会变为 CANCELLED 或 FAILED。如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE。
- APPEND_OR_REPLACE 函数类似于 APPEND,不过它并不依赖于先决条件工作状态。即使现有工作变为 CANCELLED 或 FAILED 状态,新工作仍会运行。
- work - 要调度的 WorkRequest。
对于周期性的任务只支持 REPLACE 和 KEEP 这两种策略。 对于日志上传的场景,我们只需要定义一个任务之后,希望他后续按周期执行即可。因此这里的策略设置为 KEEP 即可。
- 启动任务
由于是周期性执行的任务,我们找到一个合适的位置调用一次 triggerWork
方法即可一劳永逸了,看一下日志。
这里为了方便测试,将 uploadLog
方法中发生错误的条件改为了 System.currentTimeMillis().toInt() % 2 == 0
kotlin
21:45:36.633 25520-25579 WorkManagerPlayground com.engineer.android.mini I start upload
21:45:56.640 25520-25579 WorkManagerPlayground com.engineer.android.mini I upload fail
21:46:06.690 25520-25579 WorkManagerPlayground com.engineer.android.mini I start upload
21:46:26.694 25520-25579 WorkManagerPlayground com.engineer.android.mini I upload fail
21:46:46.775 25520-25579 WorkManagerPlayground com.engineer.android.mini I start upload
21:47:06.780 25520-25579 WorkManagerPlayground com.engineer.android.mini I finish upload
可以看到失败之后,第一次延时 10 秒后进行了重试,第二此延时是 20 秒,并且成功了。
从这个例子可以看到,WorkManager 的设计非常实用,尤其是其基于约束条件的任务调度,重试策略等。这些在实际业务开发中经常会遇到,比如网络请求失败后怎么重试?无限轮询,服务器能抗住吗?如果是由于 Bug 导致的意外失败,短时间内大规模的重试会导致雪崩,发生恶性循环将整个服务打挂。而 WorkManger 内置了线性增长或者是指数级增长的 API 就显得非常友好。这里无论是否使用 WorkManager ,看一下源码学习一下这些约束条件的实现也是很有收获的,看看 Android 团队的人是怎么写代码的,是否有值得借鉴的地方。
编排任务
除了安排周期性任务,WorkManger 另一个特点就是编排任务 的功能。这里以官方示例 WorkManagerSample ,中的代码做示范。
下面的功能是对一张图片做滤镜,首先会清理遗留文件,然后会依次执行 WaterColor/GrayScale/BlurEffec 这几个滤镜效果,而是否执行时根据传入的参数在扩展函数中做判断。最后,根据 save 字段执行 SaveImageToGalleryWorker 将图片保存在本地相册或者是上传到服务器。这个过程中,数据 InputData 会在各个任务之间自动传递,直到有错误发生或者执行成功。
kotlin
init {
continuation = WorkManager.getInstance(context).beginUniqueWork(
Constants.IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker::class.java)
).thenMaybe<WaterColorFilterWorker>(waterColor).thenMaybe<GrayScaleFilterWorker>(grayScale)
.thenMaybe<BlurEffectFilterWorker>(blur).then(
if (save) {
workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
} else {
workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
}
)
}
/**
* Applies a [ListenableWorker] to a [WorkContinuation] in case [apply] is `true`.
*/
private inline fun <reified T : ListenableWorker> WorkContinuation.thenMaybe(
apply: Boolean
): WorkContinuation {
return if (apply) {
then(workRequest<T>())
} else {
this
}
}
这里通过定义 thenMaybe
扩展函数实现了基于参数控制任务执行与否的封装。这里其实特别像 RxJava 或者是 Java StreamAPI, 通过流式 API 将一些复杂的操作串起来执行,尤其是当这些任务有依赖关系的时候,WorkManager 提供的这类编排机制就更加友好了。日常开发中,Application 中经常会有大量的初始化,而这些初始经常会有依赖关系,比如很多三方库依赖网络库的初始化,有些则依赖系统权限的获取,总之时间久了就变成一锅粥了。如果能合理的使用 WorkMangaer 对这些任务进行编排,势必会减少后期的维护成本。
关于初始化
这两篇关于 WorkManager 的文章,始终没有提及 WorkManager 的初始化。其实他和大名鼎鼎的 LeakCanary 一样,是通过 ContentProvider 进行初始化,如果你的应用对启动时间(无论是冷启动还是热启动)都非常敏感的话,可以通过以下方式移除 WorkManager 的初始化。
xml
<!-- If you want to disable android.startup completely. -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
但同时,你需要自己在合适的位置手动进行初始化。
小结
WorkManger 的功能非常强大,由于其任务是通过 SQLite 数据库持久化存储在本地,因此相比以往的线程机制,对任务的可控性强了很多,尤其是周期性的任务。无论是任务进度的查询,还是任务的取消都可以结合其 API 实现。更多的功能可以参考官方提供的指导文档,结合实际业务场景进行使用,会有更具体的体会和认识。