乐趣区

关于android:WorkManager-周期性任务

WorkManager 是一个 Android Jetpack 扩大库,它能够让您轻松布局那些可延后、异步但又须要牢靠运行的工作。对于绝大部分后盾执行工作来说,应用 WorkManager 是目前 Android 平台上的最佳实际。

如果您始终关注本系列文章,则会发现咱们曾经探讨过:

  • Android Jetpack WorkManager | Android 中文教学视频
  • WorkManager 在 Kotlin 中的实际

本文将介绍:

  • 定义周期性工作
  • 勾销工作
  • 自定义 WorkManager 配置

反复执行的工作

之前的文章中,咱们曾经介绍过应用 OneTimeWorkRequest 来布局工作。但如果您心愿工作能够周期性地反复执行,则能够应用 PeriodicWorkRequest。

让咱们先看看这两种 WorkRequest 之间的区别:

  • 最小周期时长为 15 分钟 (与 JobScheduler 雷同)
  • Worker 类不能在 PeriodicWorkRequest 中链式执行
  • 在 v2.1-alpha02 之前,无奈在创立 PeriodicWorkRequest 时设置初始提早

在与别人的探讨中,我遇到的一些常见问题与周期性工作无关。在本文中,我将会介绍周期性工作的基础知识以及常见用例和谬误。另外,我也会介绍几种为 Worker 类编写测试的形式。

API

比照以前介绍过的创立一次性工作办法,创立 PeriodicWorkRequest 的调用没有很大的不同,只是多出了一个额定的参数用来指定最小反复距离 (minimum repeat interval):

val work = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS).build()

这个参数被称为“最小距离”,是因为 Android 的电池优化策略和一些您增加的约束条件会缩短两次反复之间的工夫距离。举个例子,如果您指定某个工作只会在设施充电时运行,那么如果设施没在充电,即便过了最小距离,这个工作也不会执行——直到设施开始充电为止。

PeriodicWorkRequest 配合充电状态束缚

在这种情景下,咱们须要为 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 须要均衡两个不同的需要:利用的 WorkRequest 和 Android 零碎限度电池耗费的需要,所以即便您为 WorkRequest 设置的所有约束条件都被满足,您的 Work 在减少了一些额定提早之后仍能够被执行。

Android 蕴含了一组电池优化的策略:当用户没有应用设施时,零碎会尽量减少流动以节俭电量。这些策略会对工作的执行造成影响:在您的设施进入 低电耗模式 (Doze mode) 时,工作的执行可能会被推延到下个 保护窗口 (maintenance window)。

距离和弹性距离 (FlexInterval)

如前文所述,WorkManager 不能保障工作在准确的某个工夫去执行,但如果这是您的需要,那您可能须要寻找其余的 API。因为反复距离实际上是最小距离,所以 WorkManager 还提供了一个附加参数,您能够应用该参数来指定一个窗口,从而让 Android 能够在窗口中执行您的工作。

简而言之,您能够指定第二个距离,从而管制在反复周期内能够运行您的周期性工作的区间。而这第二个距离 (弹性距离) 的地位则始终在它所在距离的开端。

让咱们察看这样一个示例:假如您想要创立一个周期性工作,其反复周期为 30 分钟,您能够指定一个比反复周期小的弹性距离,这里设为 15 分钟。

基于以上参数,构建 PeriodicWorkPequest 的理论代码为:

val logBuilder = PeriodicWorkRequestBuilder<MyWorker>(
                         30, TimeUnit.MINUTES, 
                         15, TimeUnit.MINUTES)

 
后果是,咱们的 Worker 会在周期的后半局部执行 (弹性距离的地位总是在反复周期的开端):

距离为 30 分钟、弹性距离为 15 分钟的 PeriodicWorkRequest

别忘了,这些工夫点始终基于 WorkRequest 中所蕴含的束缚和设施所处的状态。

对于此性能,如果您想要理解更多,能够浏览 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 状态。

PeriodicWorkRequest 的状态

周期性工作不会以 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)
  }
})

如果您须要周期性 Worker 可能提供一些后果数据,上述办法可能不是您的最佳选项。一个更好的抉择是将数据通过另一个媒介进行传输,比方数据库表。

更多无关获取工作状态的信息,请参考本系列的《Android Jetpack WorkManager | Android 中文教学视频》和 WorkManager 的文档:工作状态和察看工作。

独特工作

某些 WorkManager 用例可能会陷入一种模式:当利用启动时,会在第一工夫将一些工作退出队列。这些工作可能是您想要周期执行的后盾同步工作,也可能是预约内容的下载。不论是什么,常见的的模式都是须要在利用启动的第一工夫将这些工作入队。

我曾经看到这种模式几次,在 Application#onCreate 办法中,开发者创立了 WorkRequest 并将其入队。看起来一切正常,直到您发现有些工作反复执行了很屡次。这种状况在只有不进行勾销操作便不会达到最终状态的周期性工作身上尤其容易呈现。

咱们常说,即便您的利用被敞开或者设施被重启,WorkManager 仍会保障执行您的工作。所以,在利用每次启动时都尝试将您的 Worker 退出队列,会导致每次启动都增加一个新的 WorkRequest。如果您应用的是 OneTimeWorkRequest,问题可能不大,因为一旦工作执行实现,WorkRequest 也会完结。但对于周期性工作来说,“完结”是一个齐全不同的概念,后果是您可能会轻易地将多个周期性工作反复退出队列。

针对这种状况的解决方案是,应用 WorkManager#enqueueUniquePeriodicWork()) 将您的 WorkRequest 作为独特工作 (unique Work) 退出队列:

class MyApplication: Application() {override fun onCreate() {super.onCreate()
    val myWork = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
                         .build()

    WorkManager.getInstance(this).enqueueUniquePeriodicWork(“MyUniqueWorkName”,
        ExistingPeriodicWorkPolicy.KEEP,
        myWork)
  }
}

这样就能够帮您防止工作被反复屡次退出队列。

应用 KEEP 或 REPLACE?

抉择哪种策略取决于您在 Worker 中执行什么样的操作。集体而言,我通常会应用 KEEP 策略,因为它更轻量,不用替换现有的 WorkRequest,同时,这一策略也能够防止勾销曾经在运行的 Worker。

我只会在有失当理由时才会应用 REPLACE 策略,比方,当我想要在某个 Worker 的 doWork() 办法中对它本人从新排期时。

如果您抉择应用 REPLACE 策略,您的 Worker 该当适当地解决进行状态,因为这种策略下,如果一个新的 WorkRequest 在 Worker 正在运行时退出队列,WorkManager 就可能不得不勾销正在运行的实例。不过您也应该在任何状况下都解决好进行状态,因为 Worker 正在被执行时,如果某个约束条件不再被满足,WorkManager 也可能会进行您的工作。

无关独特工作的更多信息,请参阅文档:惟一工作。

测试周期性工作

WorkManager 的测试文档 非常详尽,笼罩了根本的测试计划。在 WorkManager v2.1 公布后,您有两种形式测试您的 Worker:

  • WorkManagerTestInitHelper
  • TestWorkerBuilder 和 TestListenableWorkerBuilder

应用 WorkManagerTestInitHelper,您能够在测试您的 Worker 类时模仿提早、约束条件和周期要求被满足等状况。这种测试方法的劣势在于,它能够解决 Worker 入队本人或另一个 Worker 类的状况,正如后面示例——实现了每天大概在同一时间运行的“DailyWorker”——中所看到的。理解更多信息,请查阅:WorkManager 的测试文档。

如果您须要测试 CoroutineWorker、RxWorker 和 ListenableWorker,应用 WorkManagerTestInitHelper 会带来一些额定的复杂性,因为这时您无奈依赖它的 SynchronousExecutor。

为了更加间接地测试这几个类,WorkManager v2.1 退出了一组新的 WorkRequest 结构器:

  • TestWorkerBuilder 用于间接调用 Worker 类
  • TestListenableWorkerBuilder 用于间接调用 ListenableWorker、RxWorker 或 CoroutineWorker

这些新结构器的长处是,您能够应用它们测试任何品种的 Worker 类,因为在应用它们时,您能够间接运行对应的 Worker。

您能够通过浏览 应用 WorkManager 2.1.0 进行测试 这篇文档来理解更多,也能够查看 Sunflower 示例利用 中应用这些新的结构器进行测试的示例:

import android.content.Context
import androidx.test.core.app.ApplicationProvider
mport 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 的形式。如果您对解读 WorkManager 的性能以及撰写相干文章有更好的想法,欢迎您在 Twitter 上分割我 @pfmaggi。

WorkManager 相干资源

  • 开发者指南 | 在 WorkManager 中进行线程解决
  • 参考指南 | androidx.work
  • 发行日志 | WorkManager
  • Codelab | 应用 WorkManager 解决后台任务
  • WorkManager 的源码 (AOSP 的一部分)
  • 演讲 | 应用 WorkManager (2018 Android 开发者峰会)
  • Issue Tracker
  • [Stack Overflow 的 [android-workmanager] 标签 ](https://stackoverflow.com/que…
  • Android 开发者博客上对于 Power 的文章系列
退出移动版