WorkManager: 周期性任务
WorkManager https://developer.android.google.cn/topic/libraries/architecture/workmanager/ Android Jetpack https://developer.android.google.cn/jetpack/
如果您一直关注本系列文章,则会发现我们已经讨论过:
本文将介绍:
定义周期性任务
取消任务
自定义 WorkManager 配置
重复执行的任务
让我们先看看这两种 WorkRequest 之间的区别:
最小周期时长为 15 分钟 (与 JobScheduler 相同)
Worker 类不能在 PeriodicWorkRequest 中链式执行
在 v2.1-alpha02 之前,无法在创建 PeriodicWorkRequest 时设置初始延迟
OneTimeWorkRequest https://developer.android.google.cn/reference/androidx/work/OneTimeWorkRequest PeriodicWorkRequest https://developer.android.google.cn/reference/androidx/work/PeriodicWorkRequest JobScheduler https://developer.android.google.cn/reference/android/app/job/JobScheduler
API
val work = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
.build()
这个参数被称为 "最小间隔",是因为 Android 的电池优化策略和一些您添加的约束条件会延长两次重复之间的时间间隔。举个例子,如果您指定某个任务只会在设备充电时运行,那么如果设备没在充电,即使过了最小间隔,这个任务也不会执行——直到设备开始充电为止。
在这种情景下,我们需要为 PeriodicWorkRequest 添加一个充电状态约束 (charging constraint),并将其加入队列:
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
val work = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
val workManager = WorkManager.getInstance(context)
workManager.enqueuePeriodicWork(work)
关于如何获取 WorkManager 实例的说明:
WorkManager v2.1 已经弃用了 WorkManager#getInstance() ,转而使用 WorkManager#getInstance(context: Context)。新的方法工作方式与原来相同,不同点是它支持新的按需初始化 (on-demand initialization) 功能。接下来的内容中,我都会使用需要传入 context 的新语法来获取 WorkManager 实例。
WorkManager#getInstance()
https://developer.android.google.cn/reference/androidx/work/WorkManager.html#getInstance()
WorkManager#getInstance(context: Context)
https://developer.android.google.cn/reference/androidx/work/WorkManager.html#getInstance(android.content.Context)
按需初始化 (on-demand initialization)
https://developer.android.google.cn/topic/libraries/architecture/workmanager/advanced/custom-configuration#on-demand
一个关于 "最小间隔" 的小提醒: 由于 WorkManager 需要平衡两个不同的需求: 应用的 WorkRequest 和 Android 系统限制电池消耗的需求,所以即使您为 WorkRequest 设置的所有约束条件都被满足,您的 Work 在增加了一些额外延迟之后仍可以被执行。
Android 包含了一组电池优化的策略: 当用户没有使用设备时,系统会尽量减少活动以节省电量。这些策略会对任务的执行造成影响: 在您的设备进入低电耗模式 (Doze mode) 时,任务的执行可能会被推迟到下个维护窗口 (maintenance window)。
低电耗模式 (Doze mode)
https://developer.android.google.cn/training/monitoring-device-state/doze-standby
维护窗口 (maintenance window)
https://developer.android.google.cn/training/monitoring-device-state/doze-standby#understand_doze
间隔和弹性间隔 (FlexInterval)
如前文所述,WorkManager 不能保证任务在精确的某个时间去执行,但如果这是您的需求,那您可能需要寻找其他的 API。由于重复间隔实际上是最小间隔,所以 WorkManager 还提供了一个附加参数,您可以使用该参数来指定一个窗口,从而让 Android 可以在窗口中执行您的任务。
简而言之,您可以指定第二个间隔,从而控制在重复周期内可以运行您的周期性任务的区间。而这第二个间隔 (弹性间隔) 的位置则始终在它所在间隔的末尾。
让我们观察这样一个示例: 假设您想要创建一个周期性任务,其重复周期为 30 分钟,您可以指定一个比重复周期小的弹性间隔,这里设为 15 分钟。
基于以上参数,构建 PeriodicWorkPequest 的实际代码为:
val logBuilder = PeriodicWorkRequestBuilder<MyWorker>(
30, TimeUnit.MINUTES,
15, TimeUnit.MINUTES)
结果是,我们的 Worker 会在周期的后半部分执行 (弹性间隔的位置总是在重复周期的末尾):
别忘了,这些时间点始终基于 WorkRequest 中所包含的约束和设备所处的状态。
关于此功能,如果您想要了解更多,可以阅读 PeriodicWorkRequest.Builder 文档。
PeriodicWorkRequest.Builder 文档 https://developer.android.google.cn/reference/androidx/work/PeriodicWorkRequest.Builder
每日任务
由于周期性间隔是不精确的,您无法创建在每天指定时间执行的 PeriodicWorkRequest,即使我们放宽精度限制也不行。
您可以指定 24 小时为一个周期,但是由于任务的执行与 Android 的电池优化策略有关,您的期望值只能是 Worker 会在指定时间段附近被执行。因此其结果可能是: 您的任务会在第一天的 5:00AM、第二天的 5:25AM、第三天的 5:15AM,以及第四天的 5:30AM 被执行,以此类推。随着时间的流逝,误差会被不断累积。
目前,如果您需要在每天的大致同一时间执行某一个 Worker,那么最好的选择是使用 OneTimeWorkRequest 并设置初始延迟,这样您便可以在正确的时间执行任务:
val currentDate = Calendar.getInstance()
val dueDate = Calendar.getInstance()
// 设置在大约 05:00:00 AM 执行
dueDate.set(Calendar.HOUR_OF_DAY, 5)
dueDate.set(Calendar.MINUTE, 0)
dueDate.set(Calendar.SECOND, 0)
if (dueDate.before(currentDate)) {
dueDate.add(Calendar.HOUR_OF_DAY, 24)
}
val timeDiff = dueDate.timeInMillis — currentDate.timeInMillis
val dailyWorkRequest = OneTimeWorkRequestBuilder<DailyWorker>
.setConstraints(constraints) .setInitialDelay(timeDiff, TimeUnit.MILLISECONDS)
.addTag(TAG_OUTPUT) .build()
WorkManager.getInstance(context).enqueue(dailyWorkRequest)
这样一来便能完成第一次执行。接下来我们需要将下一个任务在当前任务成功执行完成时加入队列:
class DailyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val currentDate = Calendar.getInstance()
val dueDate = Calendar.getInstance()
// 设置在大约 05:00:00 AM 执行
dueDate.set(Calendar.HOUR_OF_DAY, 5)
dueDate.set(Calendar.MINUTE, 0)
dueDate.set(Calendar.SECOND, 0)
if (dueDate.before(currentDate)) {
dueDate.add(Calendar.HOUR_OF_DAY, 24)
}
val timeDiff = dueDate.timeInMillis — currentDate.timeInMillis
val dailyWorkRequest = OneTimeWorkRequestBuilder<DailyWorker>()
.setInitialDelay(timeDiff, TimeUnit.MILLISECONDS)
.addTag(TAG_OUTPUT)
.build()
WorkManager.getInstance(applicationContext)
.enqueue(dailyWorkRequest)
return Result.success()
}
}
请记得,执行 Worker 的实际时间取决于您在 WorkRequest 中使用的约束和 Android 平台的优化操作。
周期性任务的状态
前文已经讲过,周期性任务与一次性任务的其中一个区别便是不能通过 PeriodicWorkRequest 建立任务链。之所以存在这一约束,是因为在一个任务链中,您会在一个 Worker 的状态转变为 SUCCEEDED 时过渡到任务链中的下一个 Worker,而 PeriodicWorkRequest 没有 SUCCEEDED 状态。
周期性任务不会以 SUCCEEDED 状态结束,它会持续运行直到被取消。当您在周期性任务的 Woker 中调用 Result#success() 或 Result#failure() 时,周期性任务会回到 ENQUEUED 状态并等待下一次执行。
基于这一原因,您无法在使用周期性任务时建立任务链,使用 UniqueWorkRequest 也同样不行。这样一来,PeriodicWorkRequest 也失去了追加任务的能力: 您只能使用 KEEP 和 REPLACE,而不能使用 APPEND。
数据的输入和输出
WorkManager 允许您传递一个 Data 对象给您的 Worker,同时在 success 和 failure 方法被调用时,也会返回一个新的 Data 对象给您 (由于在您返回 Result#retry() 时 Worker 的执行是无状态的,所以此时没有数据输出选项)。
在一次性 Worker 组成的链中,一个 Worker 的返回值会成为链条中下个 Worker 的输入值。我们已经知道,周期性任务无法使用任务链条,因为其并不会以 "成功" 的状态结束——它只会被取消操作所结束。
所以,我们要在哪里看到和使用 Result#success(outData) 方法所返回的数据?
我们可以通过 PeriodicWorkRequest 的 WorkInfo 来观察这些 Data。仅在周期任务下一次被执行前,我们可以依靠判断 Worker 是否处于 ENQUEUED 状态来检查它的输出:
val myPeriodicWorkRequest =
PeriodicWorkRequestBuilder<MyPeriodicWorker>(1, TimeUnit.HOURS).build()
WorkManager.getInstance(context).enqueue(myPeriodicWorkRequest)
WorkManager.getInstance()
.getWorkInfoByIdLiveData(myPeriodicWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
if ((workInfo != null) &&
(workInfo.state == WorkInfo.State.ENQUEEDED)) {
val myOutputData = workInfo.outputData.getString(KEY_MY_DATA)
}
})
WorkManager 的文档: 任务状态和观察任务 https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/states-and-observation
独特任务
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
val myWork = PeriodicWorkRequestBuilder<MyWorker>(
1, TimeUnit.HOURS)
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
“MyUniqueWorkName”,
ExistingPeriodicWorkPolicy.KEEP,
myWork)
}
}
WorkManager#enqueueUniquePeriodicWork() https://developer.android.google.cn/reference/androidx/work/WorkManager.html#enqueueUniquePeriodicWork(java.lang.String,%20androidx.work.ExistingPeriodicWorkPolicy,%20androidx.work.PeriodicWorkRequest)
使用 KEEP 或 REPLACE?
唯一工作
https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/unique-work
测试周期性任务
WorkManager 的测试文档十分详尽,覆盖了基本的测试方案。在 WorkManager v2.1 发布后,您有两种方式测试您的 Worker:
WorkManagerTestInitHelper
TestWorkerBuilder 和 TestListenableWorkerBuilder
WorkManager 的测试文档 https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/testing WorkManagerTestInitHelper https://developer.android.google.cn/reference/androidx/work/testing/WorkManagerTestInitHelper TestWorkerBuilder https://developer.android.google.cn/reference/androidx/work/testing/TestWorkerBuilder TestListenableWorkerBuilder https://developer.android.google.cn/reference/androidx/work/testing/TestListenableWorkerBuilder
WorkManager 的测试文档 https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/integration-testing CoroutineWorker https://developer.android.google.cn/reference/kotlin/androidx/work/CoroutineWorker.html RxWorker https://developer.android.google.cn/reference/androidx/work/RxWorker.html ListenableWorker https://developer.android.google.cn/reference/androidx/work/ListenableWorker
为了更加直接地测试这几个类,WorkManager v2.1 加入了一组新的 WorkRequest 构造器:
TestWorkerBuilder 用于直接调用 Worker 类
TestListenableWorkerBuilder 用于直接调用 ListenableWorker、RxWorker 或 CoroutineWorker
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.WorkManager
import androidx.work.testing.TestListenableWorkerBuilder
import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
private lateinit var context: Context
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun testRefreshMainDataWork() {
// 获取 ListenableWorker
val worker = TestListenableWorkerBuilder<SeedDatabaseWorker>(context).build()
// 同步执行该任务
val result = worker.startWork().get()
assertThat(result, `is`(Result.success()))
}
}
使用 WorkManager 2.1.0 进行测试 https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/testing-210 Sunflower 示例应用 https://github.com/googlesamples/android-sunflower/blob/master/app/src/androidTest/java/com/google/samples/apps/sunflower/worker/SeedDatabaseWorkerTest.kt
总结
WorkManager 相关资源
开发者指南 | 在 WorkManager 中进行线程处理 https://developer.android.google.cn/topic/libraries/architecture/workmanager/ 参考指南 | androidx.work https://developer.android.google.cn/reference/androidx/work/package-summary 发行日志 | WorkManager https://developer.android.google.cn/jetpack/androidx/releases/work Codelab | 使用 WorkManager 处理后台任务 https://codelabs.developers.google.com/codelabs/android-workmanager WorkManager 的源码 (AOSP的一部分) https://android.googlesource.com/platform/frameworks/support/+/master/work 演讲 | 使用 WorkManager (2018 Android 开发者峰会) https://www.youtube.com/watch?v=83a4rYXsDs0 Issue Tracker https://issuetracker.google.com/issues?q=componentid:409906 Stack Overflow 的 [android-workmanager] 标签 https://stackoverflow.com/questions/tagged/android-workmanager Android 开发者博客上关于 Power 的文章系列 https://android-developers.googleblog.com/search/label/Power%20series
推荐阅读