JetPack中的WorkManager
2018年谷歌I/O 发布了一系列辅助android开发者的实用工具,合称Jetpack,以帮助开发者构建出色的 Android 应用。
这次发布的 Android Jetpack 组件覆盖以下 4 个方面:Architecture、Foundation、Behavior 以及 UI。
该系列博客介绍一下Jetpack中常用组件,本篇介绍LiveData、ViewModel、LifeCycle。最后借助于https://github.com/android/sunflower 来写一个完整的应用
原文 https://developer.android.com/topic/libraries/architecture/workmanager?hl=zh-cn
使用 WorkManager API 可以轻松地调度即使在应用退出或设备重启时仍应运行的可延迟异步任务。
主要功能:
- 最高向后兼容到 API 14
- 在运行 API 23 及以上级别的设备上使用 JobScheduler
- 在运行 API 14-22 的设备上结合使用 BroadcastReceiver 和 AlarmManager
- 添加网络可用性或充电状态等工作约束
- 调度一次性或周期性异步任务
- 监控和管理计划任务
- 将任务链接起来
- 确保任务执行,即使应用或设备重启也同样执行任务
- 遵循低电耗模式等省电功能
WorkManager 旨在用于可延迟运行(即不需要立即运行)并且在应用退出或设备重启时必须能够可靠运行的任务。例如:
- 向后端服务发送日志或分析数据
- 定期将应用数据与服务器同步
基础
相关类:
Worker
任务的执行者,是一个抽象类,需要继承它实现要执行的任务。WorkRequest
指定让哪个 Woker 执行任务,指定执行的环境,执行的顺序等。
要使用它的子类 OneTimeWorkRequest 或 PeriodicWorkRequest。WorkManager
管理任务请求和任务队列,发起的 WorkRequest 会进入它的任务队列。WorkStatus
包含有任务的状态和任务的信息,以 LiveData 的形式提供给观察者。
入门指南
添加依赖
implementation "androidx.work:work-runtime:2.3.2"
创建后台任务
创建一个类,继承自androidx.work.Worker
,并覆写其public Result doWork()
方法,从 doWork()
返回的 Result
会通知 WorkManager 任务是否:
- 已成功完成:
Result.success()
- 已失败:
Result.failure()
- 需要稍后重试:
Result.retry()
1 |
|
配置运行任务的方式和时间
Worker
定义工作单元,WorkRequest
则定义工作的运行方式和时间。任务可以是一次性的,也可以是周期性的。对于一次性 WorkRequest
,请使用 OneTimeWorkRequest
,对于周期性工作,请使用 PeriodicWorkRequest
。
1 |
|
将任务提交给系统
1 |
|
执行 Worker 的确切时间取决于 WorkRequest
中使用的约束以及系统优化。
定义工作请求
工作约束
可以向工作添加 Constraints
,以指明工作何时可以运行。
1 |
|
如果指定了多个约束,任务将仅在满足所有约束时才会运行。
如果在任务运行期间某个约束不再得到满足,则 WorkManager 将停止工作器。当约束继续得到满足时,系统将重新尝试执行该任务。
初始延迟
如果Worker没有约束,或者工作加入队列时所有约束均已得到满足,则系统可能会选择立即运行任务。如果不希望任务立即运行,则可以将工作指定为在经过最短的初始延迟后启动。
下面的示例展示了如何将任务设置为在加入队列后至少经过 10 分钟再运行。
1 |
|
重试和退避政策
如果需要让 WorkManager 重新尝试执行任务,可以从工作器返回 Result.retry()
。
然后,系统会根据默认的退避延迟时间和政策重新调度工作。退避延迟时间指定重试工作前的最短等待时间。退避政策定义了在后续重试的尝试过程中,退避延迟时间随时间以怎样的方式增长;默认情况下按 EXPONENTIAL
延长。
以下是自定义退避延迟时间和政策的示例。
1 |
|
定义任务的输入/输出
可以通过使用androidx.work.Data
来定义输入或者输出
在UploadWorker
中
1 |
|
在调用时
1 |
|
非常重要的一点:
按照设计,Data
对象应该很小,值可以是字符串、基元类型或数组变体。如果需要将更多数据传入和传出工作器,应该将数据放在其他位置,例如 Room 数据库。Data 对象的大小上限为 10KB。
标记工作
可以通过为任意 WorkRequest
对象分配标记字符串,按逻辑对任务进行分组。这样就可以对使用特定标记的所有任务执行操作。
例如,WorkManager.cancelAllWorkByTag(String)
会取消使用特定标记的所有任务,而 WorkManager.getWorkInfosByTagLiveData(String)
会返回 LiveData
和具有该标记的所有任务的状态列表。
1 |
|
工作状态
- 如果有尚未完成的前提性工作,则工作处于
BLOCKED
State
。 - 如果工作能够在满足
Constraints
和时机条件后立即运行,则被视为处于ENQUEUED
状态。 - 当工作器在活跃地执行时,其处于
RUNNING
State
。 - 如果工作器返回
Result.success()
,则被视为处于SUCCEEDED
状态。这是一种终止State
;只有OneTimeWorkRequest
可以进入这种State
。 - 相反,如果工作器返回
Result.failure()
,则被视为处于FAILED
状态。这也是一个终止State
;只有OneTimeWorkRequest
可以进入这种State
。所有依赖工作也会被标记为FAILED
,并且不会运行。 - 当明确取消尚未终止的
WorkRequest
时,它会进入CANCELLED
State
。所有依赖工作也会被标记为CANCELLED
,并且不会运行。
观察工作
将工作加入队列后,可以通过 WorkManager 检查其状态。相关信息在 WorkInfo
对象中提供,包括工作的 id
、标签、当前 State
和任何输出数据。
可以通过以下三种方式之一来获取 WorkInfo
:
- 对于特定的
WorkRequest
,可以利用WorkManager.getWorkInfoById(UUID)
或WorkManager.getWorkInfoByIdLiveData(UUID)
来通过WorkRequest
id
检索其WorkInfo
。 - 对于指定的标记,可以利用
WorkManager.getWorkInfosByTag(String)
或WorkManager.getWorkInfosByTagLiveData(String)
检索所有匹配的WorkRequest
的WorkInfo
对象。 - 对于唯一工作名称,可以利用
WorkManager.getWorkInfosForUniqueWork(String)
或WorkManager.getWorkInfosForUniqueWorkLiveData(String)
检索所有匹配的WorkRequest
的WorkInfo
对象。
利用每个方法的 LiveData
变量,可以通过注册监听器来观察 WorkInfo
的变化。
1 |
|
更新进度
对于使用 ListenableWorker
或 Worker
的 Java 开发者,setProgressAsync()
API 会返回 ListenableFuture
;更新进度是异步过程,因为更新过程包括将进度信息存储在数据库中。
此示例展示了一个简单的 ProgressWorker
。该 Worker
启动时将进度设置为 0,完成时将进度值更新为 100。
1 |
|
观察进度
1 |
|
链接工作
就像动画的执行可以确定顺序、依赖一样,Worker的执行也可以这么操作
可以使用 WorkManager 创建工作链并为其排队。工作链用于指定多个关联任务并定义这些任务的运行顺序。当需要以特定的顺序运行多个任务时,这尤其有用。
要创建工作链,可以使用 WorkManager.beginWith(OneTimeWorkRequest)
或 WorkManager.beginWith(List)
,这会返回 WorkContinuation
实例。
然后,可以通过 WorkContinuation
使用 WorkContinuation.then(OneTimeWorkRequest)
或 WorkContinuation.then(List)
来添加从属 OneTimeWorkRequest
。
每次调用 WorkContinuation.then(...)
都会返回一个新的 WorkContinuation
实例。如果添加了 OneTimeWorkRequest
的 List
,这些请求可能会并行运行。
最后,可以使用 WorkContinuation.enqueue()
方法为 WorkContinuation
链排队。
1 |
|
这里的filter是OneTimeWorkRequest实例对象
Input Merger
在使用 OneTimeWorkRequest
链时,父级 OneTimeWorkRequest
的输出将作为输入传递给子级。因此在上面的示例中,filter1
、filter2
和 filter3
的输出将作为输入传递给 compress
请求。
为了管理来自多个父级 OneTimeWorkRequest
的输入,WorkManager 使用 InputMerger
。
WorkManager 提供两种不同类型的 InputMerger
:
OverwritingInputMerger
会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。ArrayCreatingInputMerger
会尝试合并输入,并在必要时创建数组。
对于上面的示例,假设我们要保留所有图像滤镜的输出,则应使用 ArrayCreatingInputMerger
。
1 |
|
链接和工作状态
创建 OneTimeWorkRequest
链时,需要注意以下几点:
- 从属
OneTimeWorkRequest
仅在其所有父级OneTimeWorkRequest
都成功完成(即返回Result.success()
)时才会被解除阻塞(变为ENQUEUED
状态)。 - 如果有任何父级
OneTimeWorkRequest
失败(返回Result.failure()
),则所有从属OneTimeWorkRequest
也会被标记为FAILED
。 - 如果有任何父级
OneTimeWorkRequest
被取消,则所有从属OneTimeWorkRequest
也会被标记为CANCELLED
取消和停止工作
如果不再需要运行先前加入队列的作业,则可以申请取消。最简单的方法是使用其 id
并调用 WorkManager.cancelWorkById(UUID)
来取消单个 WorkRequest:
1 |
|
在后台,WorkManager 会检查工作的 State
。如果工作已经完成,则不会发生任何变化。否则,其状态将更改为 CANCELLED
,之后就不会运行这个工作。任何依赖于这项工作的 WorkRequests
的状态也将变为 CANCELLED
。
此外,如果工作当前的状态为 RUNNING
,则工作器也会收到对 ListenableWorker.onStopped()
的调用。替换此方法以处理任何可能的清理操作。
也可以使用 WorkManager.cancelAllWorkByTag(String)
按标记取消 WorkRequest。请注意,此方法会取消所有具有此标记的工作。此外,还可以使用 WorkManager.cancelUniqueWork(String)
取消具有唯一名称的所有工作。
停止正在运行的工作器
WorkManager 停止正在运行的工作器可能有几种不同的原因:
- 明确要求取消它(例如,通过调用
WorkManager.cancelWorkById(UUID)
取消)。 - 如果是唯一工作,使用
ExistingWorkPolicy
REPLACE
明确地将新的WorkRequest
加入队列。旧的WorkRequest
会立即被视为已终止。 - 工作约束已不再得到满足。
- 系统出于某种原因指示您的应用停止工作。如果超过 10 分钟的执行期限,可能会发生这种情况。系统将工作安排在稍后重试。
在这些情况下,会收到对 ListenableWorker.onStopped()
的调用。如果操作系统决定关闭应用,应执行清理工作并以协作方式完成工作器。例如,应该在此时或者尽早关闭数据库和文件的打开句柄。此外,如果想要确认系统是否已经停止你应用,都可以调用 ListenableWorker.isStopped()
。即使通过在调用 onStopped()
后返回 Result
来指示工作已完成,WorkManager 都会忽略该 Result
,因为工作器已经被视为停止。
重复性工作
应用有时可能需要定期运行某些任务。例如,您可能要定期备份数据、下载应用中的新鲜内容,或者上传日志到服务器。 PeriodicWorkRequest
用于这种需要定期执行的任务。需要注意的是PeriodicWorkRequest
无法和其他任务进行链接。
1 |
|
唯一工作
唯一工作是一个概念性非常强的术语,可确保一次只有一个具有特定名称的工作链。与 id
不同的是,唯一名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,唯一名称仅与“一个”工作链关联。
可以通过调用 [WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)
](https://developer.android.com/reference/androidx/work/WorkManager?hl=zh-cn#enqueueUniqueWork(java.lang.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest)) 或 [WorkManager.enqueueUniquePeriodicWork(String, ExistingPeriodicWorkPolicy, PeriodicWorkRequest)
](https://developer.android.com/reference/androidx/work/WorkManager?hl=zh-cn#enqueueUniquePeriodicWork(java.lang.String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest)) 创建唯一工作序列。第一个参数是唯一名称 - 这是我们用来标识 WorkRequest
的键。第二个参数是冲突解决策略,它指定了如果已经存在一个具有该唯一名称的未完成工作链,WorkManager 应该如何处理:
- 取消现有工作链,并将其
REPLACE
为新工作链。 KEEP
现有序列并忽略您的新请求。- 将新序列
APPEND
到现有序列,在现有序列的最后一个任务完成后运行新序列的第一个任务。您不能将APPEND
与PeriodicWorkRequest
一起使用。
当有不能够多次排队的任务时,唯一工作将非常有用。例如,如果应用需要将其数据同步到网络,可能需要对一个名为“sync”的序列进行排队,并指定当已经存在具有该名称的序列时,应该忽略新的任务。当需要逐步构建一个长任务链时,也可以利用唯一工作序列。例如,照片编辑应用可能允许用户撤消一长串操作。其中的每一项撤消操作可能都需要一些时间来完成,但必须按正确的顺序执行。在这种情况下,应用可以创建一个“撤消”链,并根据需要将每个撤消操作附加到该链上。
最后,如果需要创建一个唯一工作链,可以使用 [WorkManager.beginUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)
](https://developer.android.com/reference/androidx/work/WorkManager?hl=en#beginUniqueWork(java.lang.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest)) 代替 beginWith()
。
测试
https://developer.android.com/topic/libraries/architecture/workmanager/how-to/testing?hl=zh-cn
简介与设置
WorkManager 提供了一个 work-testing
工件,可以协助进行 Android 插桩测试中的工作器单元测试。
要使用 work-testing
工件,您应该将它作为 androidTestImplementation
依赖项添加到 build.gradle
中。
概念
work-testing
为测试模式提供了一种特殊的 WorkManager 实现,它通过使用 WorkManagerTestInitHelper
来初始化。
work-testing
工件还提供了 SynchronousExecutor
,让您可以更加轻松地以同步方式编写测试,而无需处理多个线程、锁定或锁存。
以下示例展示了如何将所有这些类一起使用。
1 |
|
构造测试
现在 WorkManager 已在测试模式中初始化,您可以测试您的工作器了。
假设您有一个 EchoWorker
,它需要一些 inputData
并简单地将其输入复制(回显)到其 outputData
。
1 |
|
基本测试
以下是一个对 EchoWorker
进行测试的 Android 插桩测试。这里的要点是,在测试模式中测试 EchoWorker
与在真实应用中使用 EchoWorker
非常相似。
1 |
|
我们来编写另一个测试,它将确保在 EchoWorker
没有获得任何输入数据时,其预期的 Result
为 Result.failure()
。
1 |
|
模拟约束、延迟和定期工作
WorkManagerTestInitHelper
为您提供了一个 TestDriver
实例,可用于模拟 initialDelay
、ListenableWorker
满足 Constraint
的条件,以及 PeriodicWorkRequest
的间隔。
测试初始延迟
Worker
可以具有初始延迟。要测试含有 EchoWorker
的 initialDelay
,而不必在测试中等待 initialDelay
,您可以使用 TestDriver
将 WorkRequest
初始延迟标记为已满足条件。
1 |
|
测试约束
TestDriver
也可用于利用 setAllConstraintsMet
将约束标记为已满足条件。以下示例展示了如何测试含有约束的 Worker
。
1 |
|
测试定期工作
1 |
|
以上