WorkManager
概览
使用 WorkManager API 可以轻松地调度即使在应用退出或设备重启时仍应运行的可延迟异步任务。
主要功能:
- 最高向后兼容到 API 14
- 在运行 API 23 (Android 6.0)及以上级别的设备上使用 JobScheduler
- 在运行 API 14-22 的设备上结合使用 BroadcastReceiver 和 AlarmManager
- 添加网络可用性或充电状态等工作约束
- 调度一次性或周期性异步任务
- 监控和管理计划任务
- 将任务链接起来
- 确保任务执行,即使应用或设备重启也同样执行任务
- 遵循低电耗模式等省电功能
WorkManager 旨在用于可延迟运行(即不需要立即运行)并且在应用退出或设备重启时必须能够可靠运行的任务。例如:
- 向后端服务发送日志或分析数据
- 定期将应用数据与服务器同步
WorkManager 不适用于应用进程结束时能够安全终止的运行中后台工作,也不适用于需要立即执行的任务。请查看后台处理指南,了解哪种解决方案符合您的需求。
使用入门
将WorkManager添加到项目中
dependencies {
def work_version = "2.3.1"
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
// optional - RxJava2 support
implementation "androidx.work:work-rxjava2:$work_version"
// optional - GCMNetworkManager support
implementation "androidx.work:work-gcm:$work_version"
// optional - Test helpers
androidTestImplementation "androidx.work:work-testing:$work_version"
}
创建后台任务
任务是使用 Worker
类定义的。doWork()
方法在 WorkManager 提供的后台线程上同步运行。
要创建后台任务,请扩展 Worker
类并替换 doWork()
方法。
例如,要创建上传图像的 Worker
,您可以执行以下操作:
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
// Do the work here--in this case, upload the images.
uploadImages()
// Indicate whether the task finished successfully with the Result
return Result.success()
}
}
从
doWork()
返回的Result
会通知 WorkManager 任务是否:
已成功完成:
Result.success()
已失败:
Result.failure()
需要稍后重试:
Result.retry()
配置运行任务的方式和时间
Worker
定义工作单元,WorkRequest
则定义工作的运行方式和时间。
任务可以是一次性的,也可以是周期性的。
一次性
WorkRequest
,请使用OneTimeWorkRequest
;周期性工作,请使用
PeriodicWorkRequest
在本例中,为 UploadWorker 构建 WorkRequest
最简单的示例为:
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>().build()
WorkRequest
中还可以包含其他信息,例如任务在运行时应遵循的约束、工作输入、延迟,以及重试工作的退避时间政策。
将任务提交给系统
定义 WorkRequest
之后,您现在可以通过 WorkManager
使用 enqueue()
方法来调度它。
WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)
后续步骤
方法指南
定义WorkRequest
工作约束
您可以向工作添加 Constraints
,以指明工作何时可以运行。
例如,您可以指定工作应仅在设备空闲且接通电源时运行。
下面的代码展示了如何将这些约束添加到 OneTimeWorkRequest
。有关所支持约束的完整列表,请参阅 Constraints.Builder
参考文档。
// Create a Constraints object that defines when the task should run
val constraints = Constraints.Builder()
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.build()
// ...then create a OneTimeWorkRequest that uses those constraints
val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>().setConstraints(constraints).build()
如果指定了多个约束,您的任务将仅在满足所有约束时才会运行。
如果在任务运行期间某个约束不再得到满足,则 WorkManager 将停止工作器。当约束继续得到满足时,系统将重新尝试执行该任务。
初始延迟
如果您的工作没有约束,或者工作加入队列时所有约束均已得到满足,则系统可能会选择立即运行任务。如果您不希望任务立即运行,则可以将工作指定为在经过最短的初始延迟后启动。
下面的示例展示了如何将任务设置为在加入队列后至少经过 10 分钟再运行。
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInitialDelay(10, TimeUnit.MINUTES)
.build()
注意:执行工作器的确切时间还取决于 WorkRequest 中使用的约束和系统优化。WorkManager 经过设计,能够在满足这些约束的情况下提供可能的最佳行为。
重试和退避政策
如果您需要让 WorkManager 重新尝试执行您的任务,可以从工作器返回 Result.retry()
。
然后,系统会根据默认的退避延迟时间和政策重新调度您的工作。退避延迟时间指定重试工作前的最短等待时间。退避政策定义了在后续重试的尝试过程中,退避延迟时间随时间以怎样的方式增长;默认情况下按 EXPONENTIAL
延长。
以下是自定义退避延迟时间和政策的示例。
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
Criteria 标准, 准则, 规范
定义任务的输入/输出
任务可能需要数据以输入参数的形式传入,或者将数据返回为结果。例如,某个任务负责处理图像上传,它要求以要上传的图像的 URI 为输入,并且可能要求用已上传图像的网址作为输出。
输入和输出值以键值对的形式存储在 Data
对象中。下面的代码展示了如何在 WorkRequest
中设置输入数据。
// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(imageData)
.build()
Worker
类可通过调用 Worker.getInputData() 访问输入参数。
类似地,Data
类可用于输出返回值。要返回 Data
对象,请将它包含到 Result
的 Result.success()
或 Result.failure()
中,如下所示。
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
// Get the input
val imageUriInput = getInputData().getString(Constants.KEY_IMAGE_URI)
// TODO: validate inputs.
// Do the work
val response = uploadFile(imageUriInput)
// Create the output of the work
val outputData = workDataOf(Constants.KEY_IMAGE_URL to response.imageUrl)
// Return the output
return Result.success(outputData)
}
}
注意:按照设计,Data
对象应该很小,值可以是字符串、基元类型或数组变体。如果需要将更多数据传入和传出工作器,应该将数据放在其他位置,例如 Room 数据库。Data 对象的大小上限为 10KB。
标记工作
可以通过为任意 WorkRequest
对象分配标记字符串,按逻辑对任务进行分组。这样就可以对使用特定标记的所有任务执行操作。
例如,WorkManager.cancelAllWorkByTag(String)
会取消使用特定标记的所有任务,而 WorkManager.getWorkInfosByTagLiveData(String)
会返回 LiveData
和具有该标记的所有任务的状态列表。
以下代码展示了如何使用 WorkRequest.Builder.addTag(String)
向任务添加“cleanup”标记:
val cacheCleanupTask =
OneTimeWorkRequestBuilder<CacheCleanupWorker>()
.setConstraints(constraints)
.addTag("cleanup")
.build()
观察工作状态
工作状态
在工作的整个生命周期内,它会经历多个不同的 State
- 如果有尚未完成的前提性工作,则工作处于
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
的变化。例如,如果您想要在某项工作成功完成后向用户显示消息,您可以进行如下设置:
WorkManager.getInstance(myContext).getWorkInfoByIdLiveData(uploadWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
displayMessage("Work finished!")
}
})
观察工作器的中间进度
WorkManager
2.3.0-alpha01
为设置和观察工作器的中间进度添加了一流支持。如果应用在前台运行时,工作器保持运行状态,也可以使用返回WorkInfo
的LiveData
的 API 向用户显示此信息。
ListenableWorker
现在支持setProgressAsync()
API,此类 API 可以保留中间进度。借助这些 API,开发者能够设置可通过界面观察到的中间进度。进度由Data
类型表示,这是一个可序列化的属性容器(类似于input
和output
,并且受到相同的限制)。只有在
ListenableWorker
运行时才能观察到和更新进度信息。如果尝试在ListenableWorker
完成执行后在其中设置进度,则将会被忽略。您还可以使用getWorkInfoBy…()
或getWorkInfoBy…LiveData()
方法来观察进度信息。这两个方法会返回WorkInfo
的实例,后者有一个返回Data
的新getProgress()
方法。
更新进度
对于使用 ListenableWorker
或 Worker
的 Java 开发者,setProgressAsync()
API 会返回 ListenableFuture<Void>
;更新进度是异步过程,因为更新过程包括将进度信息存储在数据库中。在 Kotlin 中,您可以使用 CoroutineWorker
对象的 setProgress()
扩展函数来更新进度信息。
此示例展示了一个简单的 ProgressWorker
。该 Worker
启动时将进度设置为 0,完成时将进度值更新为 100。
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import kotlinx.coroutines.delay
class ProgressWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
companion object {
const val Progress = "Progress"
private const val delayDuration = 1L
}
override suspend fun doWork(): Result {
val firstUpdate = workDataOf(Progress to 0)
val lastUpdate = workDataOf(Progress to 100)
setProgress(firstUpdate)
delay(delayDuration)
setProgress(lastUpdate)
return Result.success()
}
}
观察进度
观察进度信息也很简单。您可以使用 getWorkInfoBy…()
或 getWorkInfoBy…LiveData()
方法,并引用 WorkInfo
。
以下是使用 getWorkInfoByIdLiveData
API 的示例。
WorkManager.getInstance(applicationContext)
// requestId is the WorkRequest id
.getWorkInfoByIdLiveData(requestId)
.observe(observer, Observer { workInfo: WorkInfo? ->
if (workInfo != null) {
val progress = workInfo.progress
val value = progress.getInt(Progress, 0)
// Do something with progress information
}
})
将工作链接在一起
简介
使用 WorkManager 创建工作链并为其排队。工作链用于指定多个关联任务并定义这些任务的运行顺序。当您需要以特定的顺序运行多个任务时,这尤其有用。
要创建工作链,您可以使用 WorkManager.beginWith(OneTimeWorkRequest)
或 WorkManager.beginWith(List)
,这会返回 WorkContinuation
实例。
然后,可以通过 WorkContinuation
使用 WorkContinuation.then(OneTimeWorkRequest)
或 WorkContinuation.then(List)
来添加从属 OneTimeWorkRequest
。
每次调用 WorkContinuation.then(...)
都会返回一个新的 WorkContinuation
实例。如果添加了 OneTimeWorkRequest
的 List
,这些请求可能会并行运行。
最后,您可以使用 WorkContinuation.enqueue()
方法为 WorkContinuation
链排队。
让我们看一个示例:某个应用对 3 个不同的图像执行图像滤镜(可能会并行执行),然后将这些图像压缩在一起,再上传它们。
WorkManager.getInstance(myContext)
// Candidates to run in parallel
.beginWith(listOf(filter1, filter2, filter3))
// Dependent work (only runs after all previous work in chain)
.then(compress)
.then(upload)
// Don't forget to enqueue()
.enqueue()
Input Merger
在使用 OneTimeWorkRequest
链时,父级 OneTimeWorkRequest
的输出将作为输入传递给子级。因此在上面的示例中,filter1
、filter2
和 filter3
的输出将作为输入传递给 compress
请求。
为了管理来自多个父级 OneTimeWorkRequest
的输入,WorkManager 使用 InputMerger
。
WorkManager 提供两种不同类型的 InputMerger
:
OverwritingInputMerger
会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。ArrayCreatingInputMerger
会尝试合并输入,并在必要时创建数组。
对于上面的示例,假设我们要保留所有图像滤镜的输出,则应使用 ArrayCreatingInputMerger
。
val compress: OneTimeWorkRequest = OneTimeWorkRequestBuilder<CompressWorker>()
.setInputMerger(ArrayCreatingInputMerger::class)
.setConstraints(constraints)
.build()
链接和工作状态
创建 OneTimeWorkRequest
链时,需要注意以下几点:
- 从属
OneTimeWorkRequest
仅在其所有父级OneTimeWorkRequest
都成功完成(即返回Result.success()
)时才会被解除阻塞(变为ENQUEUED
状态)。 - 如果有任何父级
OneTimeWorkRequest
失败(返回Result.failure()
),则所有从属OneTimeWorkRequest
也会被标记为FAILED
。 - 如果有任何父级
OneTimeWorkRequest
被取消,则所有从属OneTimeWorkRequest
也会被标记为CANCELLED
。
取消和停止工作
如果您不再需要运行先前加入队列的作业,则可以申请取消。最简单的方法是使用其 id
并调用 WorkManager.cancelWorkById(UUID)
来取消单个 WorkRequest:
WorkManager.cancelWorkById(workRequest.id)
在后台,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
无法链接。如果您的任务需要链接任务,请考虑 OneTimeWorkRequest
。
您可以按照以下方式创建 PeriodicWorkRequest:
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
val saveRequest =
PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(myContext)
.enqueue(saveRequest)
示例中展示了一个重复间隔为一小时的定期工作请求。
重复间隔定义为重复之间的最短时间。工作器的确切执行时间取决于您在工作请求中使用的约束,也取决于系统进行的优化。
在示例中,PeriodicWorkRequest 还要求设备接通电源。在这种情况下,即使过了定义的一小时重复间隔,PeriodicWorkRequest 也将在设备接通电源时运行。
注意:可以定义的最短重复间隔是 15 分钟(与 JobScheduler API 相同)。
处理唯一作业
唯一工作是一个概念性非常强的术语,可确保一次只有一个具有特定名称的工作链。与
id
不同的是,唯一名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,唯一名称仅与“一个”工作链关联。
您可以通过调用 [WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)
](https://developer.android.com/reference/androidx/work/WorkManager#enqueueUniqueWork(java.lang.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest)) 或 [WorkManager.enqueueUniquePeriodicWork(String, ExistingPeriodicWorkPolicy, PeriodicWorkRequest)
](https://developer.android.com/reference/androidx/work/WorkManager#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()
。
测试Worker实现
从2.1.0版本开始,WorkerManager提供了APIs,用来测试Woker,ListenableWorker,以及ListenableWorker的子类(CoroutineWorker / RxWorker / Worker)
在V2.1.0之前,如果要测试Workers,需要使用WorkManagerTestInitHelper去初始化WorkerManager。通过V2.1.0,不需要WorkerManagerTestInitHelper,就可以测试Woker的实现。
测试ListenableWorker及其子类
测试ListenableWorker或者它的子类(CoroutineWorker / RxWorker),使用TestListenableWorkerBuilder。这个构建器可以帮助构建ListenableWorker的实例,用于测试Worker的业务逻辑。
比如,假设我们需要测试一个如下的CoroutineWorker:
class SleepWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
override suspend fun doWork(): Result {
delay(1000) // milliseconds
return Result.success()
}
}
为了测试SleepWorker,我们首先通过TestListenableWorkerBuilder创建Worker的实例。这个构建器也可以用来设置标签、inputData、runAttemptCount等等。具体请参考 TestListenableWorker
@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun testSleepWorker() {
// Kotlin code can use the TestListenableWorkerBuilder extension to
// build the ListenableWorker
val worker = TestListenableWorkerBuilder<SleepWorker>(context).build()
runBlocking {
val result = worker.doWork()
assertThat(result, `is`(Result.success()))
}
}
}
测试Workers
假设我们有一个Worker,如下所示
class SleepWorker(context: Context, parameters: WorkerParameters) :
Worker(context, parameters) {
companion object {
const val SLEEP_DURATION = "SLEEP_DURATION"
}
override fun doWork(): Result {
// Sleep on a background thread.
val sleepDuration = inputData.getLong(SLEEP_DURATION, 1000)
Thread.sleep(sleepDuration)
return Result.success()
}
}
为了测试这个Worker,现在我们可以使用TestWorkerBuilder。TestWorkerBuilder和TestListenableWorkerBuilder的主要区别在于,TestWorkerBuilder允许你指定Executor来运行Worker
// Kotlin code can use the TestWorkerBuilder extension to
// build the Worker
@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
private lateinit var context: Context
private lateinit var executor: Executor
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
executor = Executors.newSingleThreadExecutor()
}
@Test
fun testSleepWorker() {
val worker = TestWorkerBuilder<SleepWorker>(
context = context,
executor = executor,
inputData = workDataOf("SLEEP_DURATION" to 10000L)
).build()
val result = worker.doWork()
assertThat(result, `is`(Result.success()))
}
}
使用WorkManager进行集成测试
介绍和设置
WorkManager提供了一个work-testing神器,可以帮助Wokers进行Android Instrumentation测试的单元测试。
为了使用work-testing神器,需要在build.gradle添加依赖
// optional - Test helpers
androidTestImplementation "androidx.work:work-testing:$work_version"
注意:从2.1.0开始,WorkManager提供了新的TestWorkerBuilder和TestListenableWorkerBuilder类,这两个类可以让你在不需要用WorkManagerTestInitHelper初始化WorkManager的情况下,就可以测试Worker中的业务逻辑。本页中的材料对于你需要在Worker实现之外进行Worker测试时仍然很有用。
概念
测试模式下,work-testing提供了WorkerManager的特殊实现,通过WorkerManagerTestInitHelper初始化。
work-testing还提供了一个SynchronousExecutor,它让我们可以更容易地以同步的方式编写测试,而不需要处理多个线程、锁或锁的问题。
通过下面的例子,来展示一起使用这些类:
@RunWith(AndroidJUnit4::class)
class BasicInstrumentationTest {
@Before
fun setup() {
val context = InstrumentationRegistry.getTargetContext()
val config = Configuration.Builder()
// Set log level to Log.DEBUG to make it easier to debug
.setMinimumLoggingLevel(Log.DEBUG)
// Use a SynchronousExecutor here to make it easier to write tests
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
}
结构化测试
假设我们有一个EchoWorker,期望有一些输入数据,并简单地将其复制(回传)到其输出数据
class EchoWorker(context: Context, parameters: WorkerParameters)
: Worker(context, parameters) {
override fun doWork(): Result {
return when(inputData.size()) {
0 -> Result.failure()
else -> Result.success(inputData)
}
}
}
基本测试
下面是一个测试EchoWorker的Android Instrumentation测试。这里的要点是,在测试模式下测试EchoWorker与实际应用中使用EchoWorker的方式非常相似。
@Test
@Throws(Exception::class)
fun testSimpleEchoWorker() {
// Define input data
val input = workDataOf(KEY_1 to 1, KEY_2 to 2)
// Create request
val request = OneTimeWorkRequestBuilder<EchoWorker>()
.setInputData(input)
.build()
val workManager = WorkManager.getInstance(applicationContext)
// Enqueue and wait for result. This also runs the Worker synchronously
// because we are using a SynchronousExecutor.
workManager.enqueue(request).result.get()
// Get WorkInfo and outputData
val workInfo = workManager.getWorkInfoById(request.id).get()
val outputData = workInfo.outputData
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
assertThat(outputData, `is`(input))
}
当EchoWorker没有获取到任何输入数据的时候,我们希望返回的Result是Result.failure()
@Test
@Throws(Exception::class)
fun testEchoWorkerNoInput() {
// Create request
val request = OneTimeWorkRequestBuilder<EchoWorker>()
.build()
val workManager = WorkManager.getInstance(applicationContext)
// Enqueue and wait for result. This also runs the Worker synchronously
// because we are using a SynchronousExecutor.
workManager.enqueue(request).result.get()
// Get WorkInfo
val workInfo = workManager.getWorkInfoById(request.id).get()
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.FAILED))
}
模拟约束、延迟和周期性工作
WorkManagerTestInitHelper为你提供了一个TestDriver的实例,它可以用来模拟初始化延迟、ListenableWorkers满足约束的条件,以及PeriodicWorkRequests的时间间隔。
测试初始化延迟
Worker可以有初始延迟。要测试EchoWorker的初始延迟,相比于在测试中等待初始延迟,我们更倾向于使用TestDriver将WorkRequests的初始延迟标记为满足。
@Test
@Throws(Exception::class)
fun testWithInitialDelay() {
// Define input data
val input = workDataOf(KEY_1 to 1, KEY_2 to 2)
// Create request
val request = OneTimeWorkRequestBuilder<EchoWorker>()
.setInputData(input)
.setInitialDelay(10, TimeUnit.SECONDS)
.build()
val workManager = WorkManager.getInstance(getApplicationContext())
val testDriver = WorkManagerTestInitHelper.getTestDriver()
// Enqueue and wait for result.
workManager.enqueue(request).result.get()
testDriver.setInitialDelayMet(request.id)
// Get WorkInfo and outputData
val workInfo = workManager.getWorkInfoById(request.id).get()
val outputData = workInfo.outputData
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
assertThat(outputData, `is`(input))
}
测试约束
TestDriver通过setAllConstraintsMet方法,可以标记约束条件被满足。
下面是一个关于如何用约束来测试Worker的例子。
@Test
@Throws(Exception::class)
fun testWithConstraints() {
// Define input data
val input = workDataOf(KEY_1 to 1, KEY_2 to 2)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// Create request
val request = OneTimeWorkRequestBuilder<EchoWorker>()
.setInputData(input)
.setConstraints(constraints)
.build()
val workManager = WorkManager.getInstance(myContext)
val testDriver = WorkManagerTestInitHelper.getTestDriver()
// Enqueue and wait for result.
workManager.enqueue(request).result.get()
testDriver.setAllConstraintsMet(request.id)
// Get WorkInfo and outputData
val workInfo = workManager.getWorkInfoById(request.id).get()
val outputData = workInfo.outputData
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
assertThat(outputData, `is`(input))
}
测试周期性工作
TestDriver还公开了一个setPeriodDelayMet,它可以用来表示一个间隔已经完成。
下面是一个使用setPeriodDelayMet的例子。
@Test
@Throws(Exception::class)
fun testPeriodicWork() {
// Define input data
val input = workDataOf(KEY_1 to 1, KEY_2 to 2)
// Create request
val request = PeriodicWorkRequestBuilder<EchoWorker>(15, MINUTES)
.setInputData(input)
.build()
val workManager = WorkManager.getInstance(myContext)
val testDriver = WorkManagerTestInitHelper.getTestDriver()
// Enqueue and wait for result.
workManager.enqueue(request).result.get()
// Tells the testing framework the period delay is met
testDriver.setPeriodDelayMet(request.id)
// Get WorkInfo and outputData
val workInfo = workManager.getWorkInfoById(request.id).get()
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.ENQUEUED))
}
调试WorkManager
高级概念
配置和初始化
默认情况下,当您的应用启动时,WorkManager 使用适合大多数应用的合理选项自动进行配置。如果您需要进一步控制 WorkManager 管理和调度工作的方式,可以通过自己初始化 WorkManager 自定义 WorkManager 配置。
WorkManager 2.1.0及更高版本
WorkManager 2.1.0 有多种配置 WorkManager 的方式。为 WorkManager 提供自定义初始化的最灵活方式是使用 WorkManager 2.1.0 及更高版本中提供的按需初始化。
按需初始化
通过按需初始化,您可以仅在需要 WorkManager 时创建该组件,而不必每次应用启动时都创建。这样做可将 WorkManager 从关键启动路径中移出,从而提高应用启动性能。
移除默认初始化程序
要提供自己的配置,必须先移除默认初始化程序。为此,请使用合并规则 tools:node="remove"
更新 AndroidManifest.xml
:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
要详细了解如何在清单中使用合并规则,请参阅有关合并多个清单文件的文档。
实现Configration.Provider
让您的 Application
类实现 Configuration.Provider
接口,并提供您自己的 Configuration.Provider.getWorkManagerConfiguration()
实现。
当您需要使用 WorkManager 时,请确保调用方法 WorkManager.getInstance(Context)
。WorkManager 调用应用的自定义 getWorkManagerConfiguration()
方法来发现其 Configuration
。(您无需自己调用 WorkManager.initialize()
。)
注意:如果您在 WorkManager 初始化之前调用已弃用的无参数 WorkManager.getInstance()
方法,该方法将抛出异常。您应始终使用 WorkManager.getInstance(Context)
方法,即使您不自定义 WorkManager。
以下示例展示了自定义 getWorkManagerConfiguration()
实现:
class myApplication() : Application(), Configuration.Provider {
override getWorkManagerConfiguration() =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build()
}
WorkManager 2.0.1及更早版本
默认初始化
当您的应用启动时,WorkManager 使用自定义
ContentProvider
进行初始化。此代码位于内部类androidx.work.impl.WorkManagerInitializer
中,并使用默认Configuration
。自动使用默认初始化程序(除非明确停用它)。默认初始化程序适合大多数应用。
自定义初始化
如果您想控制初始化进程,则必须停用默认初始化程序,然后定义您自己的自定义配置。
移除默认初始化程序后,可以手动初始化 WorkManager:
// provide custom configuration
val myConfig = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build()
// initialize WorkManager
WorkManager.initialize(this, myConfig)
WorkManager
单例。确保初始化在 Application.onCreate()
或 ContentProvider.onCreate()
中运行。
有关可用自定义的完整列表,请参阅 Configuration.Builder()
参考文档。
WorkManager中的线程处理
概览
WorkManager 提供了四种不同类型的工作基元:
Worker
是最简单的实现,前面几节已经有所介绍。WorkManager 会在后台线程上自动运行它(您可以将它替换掉)。请参阅工作器中的线程处理,详细了解Worker
中的线程处理。- 建议 Kotlin 用户实现
CoroutineWorker
。CoroutineWorker
针对后台工作公开挂起函数。默认情况下,它们运行默认的Dispatcher
,您可以对其进行自定义。请参阅 CorventineWorker 中的线程处理,详细了解CoroutineWorker
中的线程处理。 - 建议 RxJava2 用户实现
RxWorker
。如果您有很多现有异步代码是用 RxJava 建模的,则应使用 RxWirkers。与所有 RxJava2 概念一样,您可以自由选择所需的线程处理策略。请参阅 RxWorker 中的线程处理,详细了解RxWorker
中的线程处理。 ListenableWorker
是Worker
、CoroutineWorker
和RxWorker
的基类。该类专为需要与基于回调的异步 API(例如FusedLocationProviderClient
)进行交互并且不使用 RxJava2 的 Java 开发者而设计。请参阅 ListenableWorker 中的线程处理,详细了解ListenableWorker
中的线程处理。
用Worker处理线程
当您使用 Worker
时,WorkManager 会自动在后台线程中调用 Worker.doWork()
。该后台线程来自于 WorkManager 的 Configuration
中指定的 Executor
。
默认情况下,WorkManager 会为您设置 Executor
,但您也可以自己进行自定义。
例如,您可以在应用中共享现有的后台 Executor
,也可以创建单线程 Executor
以确保所有后台工作都按顺序执行,甚至可以指定一个具有不同线程数的 ThreadPool
。
要自定义 Executor
,请确保您已启用 WorkManager 的手动初始化。在配置 WorkManager 时,您可以按以下方式指定 Executor
:
WorkManager.initialize(
context,
Configuration.Builder()
.setExecutor(Executors.newFixedThreadPool(8))
.build())
下面是一个简单的工作器示例,它按顺序下载某些网站的内容:
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): ListenableWorker.Result {
for (i in 0..99) {
try {
downloadSynchronously("https://www.google.com")
} catch (e: IOException) {
return ListenableWorker.Result.failure()
}
}
return ListenableWorker.Result.success()
}
}
请注意,
Worker.doWork()
是同步调用 - 您将会以阻塞方式完成整个后台工作,并在方法退出时完成工作。如果您在doWork()
中调用异步 API 并返回Result
,则回调可能无法正常运行。如果您遇到这种情况,请考虑使用ListenableWorker
(请参阅在 ListenableWorker 中进行线程处理)。
当前正在运行的 Worker
因为任何原因而停止时,它会收到对 Worker.onStopped()
的调用。替换此方法或在代码的检查点处调用 Worker.isStopped()
,并在必要时释放资源。当上述示例中的 Worker
被停止时,下载项目可能才下载了一半,并且会继续下载,即使已经被停止也不受影响。要优化此行为,您可以执行以下操作:
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): ListenableWorker.Result {
for (i in 0..99) {
if (isStopped) {
break
}
try {
downloadSynchronously("https://www.google.com")
} catch (e: IOException) {
return ListenableWorker.Result.failure()
}
}
return ListenableWorker.Result.success()
}
}
Worker
停止后,从 Worker.doWork()
返回什么已不重要;Result
将被忽略。
用CoroutineWorker处理线程
对于 Kotlin 用户,WorkManager 为协程提供了一流的支持。要开始使用,请将 work-runtime-ktx
包含到您的 gradle 文件中。不要扩展 Worker
,而应扩展 CoroutineWorker
,后者使用的 API 略有不同。例如,如果要构建简单的 CoroutineWorker
来执行某些网络操作,则需要执行以下操作:
class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = coroutineScope {
val jobs = (0 until 100).map {
async {
downloadSynchronously("https://www.google.com")
}
}
// awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
jobs.awaitAll()
Result.success()
}
}
请注意,CoroutineWorker.doWork()
是一个“挂起”函数。不同于 Worker
,此代码不会在 Configuration
中指定的 Executor
上运行,而是默认为 Dispatchers.Default
。您可以提供自己的 CoroutineContext
来自定义这个行为。在上面的示例中,您可能希望在 Dispatchers.IO
上完成此操作,如下所示:
class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override val coroutineContext = Dispatchers.IO
override suspend fun doWork(): Result = coroutineScope {
val jobs = (0 until 100).map {
async {
downloadSynchronously("https://www.google.com")
}
}
// awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
jobs.awaitAll()
Result.success()
}
}
CoroutineWorker
通过取消协程并传播取消信号来自动处理停工情况。您无需执行任何特殊操作来处理停工情况。
用RxWorker处理线程
我们提供了 WorkManager 与 RxJava2 之间的互操作性。要开始使用这种互操作性,除了 work-runtime
,还应将 work-rxjava2
包含到 gradle 文件中。然后,您应该扩展 RxWorker
,而不是扩展 Worker
。最后替换 RxWorker.createWork()
方法以返回 Single<Result>
,用于表示您执行的 Result
,如下所示:
public class RxDownloadWorker extends RxWorker {
public RxDownloadWorker(Context context, WorkerParameters params) {
super(context, params);
}
@Override
public Single<Result> createWork() {
return Observable.range(0, 100)
.flatMap { download("https://www.google.com") }
.toList()
.map { Result.success() };
}
}
请注意,RxWorker.createWork()
在主线程上调用,但默认情况下会在后台线程上订阅返回值。您可以替换 RxWorker.getBackgroundScheduler()
来更改订阅线程。
停止 RxWorker
会妥善处理 Observer
,因此您无需以任何特殊方式处理停工。
用ListenableWorker处理线程
在某些情况下,您可能需要提供自定义线程处理策略。例如,您可能需要处理基于回调的异步操作。在这种情况下,不能只依靠 Worker
来完成操作,因为它无法以阻塞方式完成这项任务。WorkManager 通过 ListenableWorker
支持该用例。ListenableWorker
是最低层级的工作器 API;Worker
、CoroutineWorker
和 RxWorker
都是从这个类衍生而来的。ListenableWorker
只会发出信号以表明应该开始和停止工作,而线程处理则完全交由您负责完成。开始工作信号在主线程上调用,因此请务必手动转到您选择的后台线程。
抽象方法 ListenableWorker.startWork()
会返回一个将使用操作的 Result
设置的 ListenableFuture
。ListenableFuture
是一个轻量级接口:它是一个 Future
,用于提供附加监听器和传播异常的功能。在 startWork
方法中,应该返回 ListenableFuture
,完成操作后,您需要使用操作的 Result
设置这个返回结果。您可以通过以下两种方式创建 ListenableFuture
:
- 如果您使用的是 Guava,请使用
ListeningExecutorService
。 - 否则,请将
councurrent-futures
包含到您的 gradle 文件并使用CallbackToFutureAdapter
。
如果您希望基于异步回调执行某些工作,可以执行如下操作:
public class CallbackWorker extends ListenableWorker {
public CallbackWorker(Context context, WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public ListenableFuture<Result> startWork() {
return CallbackToFutureAdapter.getFuture(completer -> {
Callback callback = new Callback() {
int successes = 0;
@Override
public void onFailure(Call call, IOException e) {
completer.setException(e);
}
@Override
public void onResponse(Call call, Response response) {
++successes;
if (successes == 100) {
completer.set(Result.success());
}
}
};
for (int i = 0; i < 100; ++i) {
downloadAsynchronously("https://www.google.com", callback);
}
return callback;
});
}
}
如果您的工作停止会发生什么?如果预计工作会停止,则始终会取消 ListenableWorker
的 ListenableFuture
。通过使用 CallbackToFutureAdapter
,您只需添加一个取消监听器即可,如下所示:
public class CallbackWorker extends ListenableWorker {
public CallbackWorker(Context context, WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public ListenableFuture<Result> startWork() {
return CallbackToFutureAdapter.getFuture(completer -> {
Callback callback = new Callback() {
int successes = 0;
@Override
public void onFailure(Call call, IOException e) {
completer.setException(e);
}
@Override
public void onResponse(Call call, Response response) {
++successes;
if (successes == 100) {
completer.set(Result.success());
}
}
};
completer.addCancellationListener(cancelDownloadsRunnable, executor);
for (int i = 0; i < 100; ++i) {
downloadAsynchronously("https://www.google.com", callback);
}
return callback;
});
}
}
支持长时间运行的工作器
WorkManager 2.3.0-alpha02增加了对长期运行的工作者的一流支持。在这种情况下,WorkManager可以向操作系统提供一个信号,即如果可能的话,在执行该工作的同时,应该保持进程的活力。这些Worker的运行时间可以超过10分钟。这个新功能的用例包括批量上传或下载(不能分组),在本地进行ML模型的压缩,或对应用的用户来说很重要的任务。
在引擎盖下,WorkManager代表你管理并运行一个前台服务来执行WorkRequest,同时也会显示一个可配置的通知。
ListenableWorker现在支持setForegroundAsync()API,而CoroutineWorker则支持暂停setForeground()API。这些API允许开发人员指定这个WorkRequest是重要的(从用户的角度来看)或长期运行的WorkRequest。
从 2.3.0-alpha03 版本开始,WorkManager还允许用户创建一个PendingIntent,无需使用createCancelPendingIntent()API注册一个新的Android组件,就可以用来取消Worker。这种方法在与setForegroundAsync()或setForeground()API一起使用时特别有用,可以用来添加一个取消Worker的通知动作。
创建和管理长时间运行的任务
Java
使用ListenableWorker或Worker的开发者可以调用setForegroundAsync()API,返回一个ListenableFuture。你也可以调用setForegroundAsync(),来更新一个正在运行的Notification。
下面是一个简单的例子,它是一个长期运行的worker下载文件。这个Worker会跟踪进度,更新一个正在进行的Notification,显示下载进度。
public class DownloadWorker extends Worker {
private static final String KEY_INPUT_URL = "KEY_INPUT_URL";
private static final String KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME";
private NotificationManager notificationManager;
public DownloadWorker(
@NonNull Context context,
@NonNull WorkerParameters parameters) {
super(context, parameters);
notificationManager = (NotificationManager)
context.getSystemService(NOTIFICATION_SERVICE);
}
@NonNull
@Override
public Result doWork() {
Data inputData = getInputData();
String inputUrl = inputData.getString(KEY_INPUT_URL);
String outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME);
// Mark the Worker as important
String progress = "Starting Download";
setForegroundAsync(createForegroundInfo(progress));
download(inputUrl, outputFile);
return Result.success();
}
private void download(String inputUrl, String outputFile) {
// Downloads a file and updates bytes read
// Calls setForegroundInfoAsync() periodically when it needs to update
// the ongoing Notification
}
@NonNull
private ForegroundInfo createForegroundInfo(@NonNull String progress) {
// Build a notification using bytesRead and contentLength
Context context = getApplicationContext();
String id = context.getString(R.string.notification_channel_id);
String title = context.getString(R.string.notification_title);
String cancel = context.getString(R.string.cancel_download);
// This PendingIntent can be used to cancel the worker
PendingIntent intent = WorkManager.getInstance(context)
.createCancelPendingIntent(getId());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel();
}
Notification notification = new NotificationCompat.Builder(context, id)
.setContentTitle(title)
.setTicker(title)
.setSmallIcon(R.drawable.ic_work_notification)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build();
return new ForegroundInfo(notification);
}
@RequiresApi(Build.VERSION_CODES.O)
private void createChannel() {
// Create a Notification channel
}
}
Kotlin
Kotlin开发者应该使用CoroutineWorker,而不是使用setForegroundAsync(),你可以使用该方法的悬浮版本的setForeground()来代替。
class DownloadWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
override suspend fun doWork(): Result {
val inputUrl = inputData.getString(KEY_INPUT_URL)
?: return Result.failure()
val outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME)
?: return Result.failure()
// Mark the Worker as important
val progress = "Starting Download"
setForeground(createForegroundInfo(progress))
download(inputUrl, outputFile)
return Result.success()
}
private fun download(inputUrl: String, outputFile: String) {
// Downloads a file and updates bytes read
// Calls setForegroundInfo() periodically when it needs to update
// the ongoing Notification
}
// Creates an instance of ForegroundInfo which can be used to update the
// ongoing notification.
private fun createForegroundInfo(progress: String): ForegroundInfo {
val id = applicationContext.getString(R.string.notification_channel_id)
val title = applicationContext.getString(R.string.notification_title)
val cancel = applicationContext.getString(R.string.cancel_download)
// This PendingIntent can be used to cancel the worker
val intent = WorkManager.getInstance(applicationContext)
.createCancelPendingIntent(getId())
// Create a Notification channel if necessary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel()
}
val notification = NotificationCompat.Builder(applicationContext, id)
.setContentTitle(title)
.setTicker(title)
.setContentText(progress)
.setSmallIcon(R.drawable.ic_work_notification)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build()
return ForegroundInfo(notification)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
// Create a Notification channel
}
companion object {
const val KEY_INPUT_URL = "KEY_INPUT_URL"
const val KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME"
}
}