插图来自 Virginia Poltrack
欢迎来到我们 WorkManager 系列的第二篇文章。WorkManager 是一个 Android Jetpack 库,当满足工作的约束条件时,用来运行可延迟、需要保障的后台工作。对于许多类型的后台工作,WorkManager 是当前的最佳实践方案。在第一篇博文中,我们讨论了 WorkManager 是什么以及何时使用 WorkManager。
在这篇博文中,我将介绍:
我还将解释 WorkManager 幕后发生的事情,以便你可以就如何使用它做出明智的决定。
假设你有一个图片编辑应用,可让你给图像加上滤镜并将其上传到网络让全世界看到。你希望创建一系列后台任务,这些任务用于滤镜,压缩图像和之后的上传。在每个环节,都有一个需要检查的约束——给图像加滤镜时要有足够的电量,压缩图像时要有足够的存储空间,以及上传图像时要有网络连接。
这个例子如上图所示
这个例子正是具有以下特点的任务:
这些特点使我们的图像加滤镜和上传任务成为 WorkManager 的完美用例。
本文使用 Kotlin 书写代码,使用 KTX 库(KoTlin eXtensions)。KTX 版本的库提供了 扩展函数 为了更简洁和习惯的使用 Kotlin。你可以添加如下依赖来使用 KTX 版本的 WorkManager:
dependencies {
def work_version = "1.0.0-beta02"
implementation "android.arch.work:work-runtime-ktx:$work_version"
}
复制代码
你可以在 这里](developer.android.com/topic/libra…) 到该库的最新版本。如果你想使用 Java 依赖,那就移除“-ktx”。
在我们将多个任务连接在一起之前,让我们关注如何执行一项工作。我将会着重细说上传任务。首先,你需要创建自己的 Worker
实现类。我将会把我们的类命名为 UploadWorker
,然后重写 doWork()
方法。
Worker
s:
这是一个示例,展示了如何实现上传图像的 Worker
:
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
try {
// Get the input
val imageUriInput = inputData.getString(Constants.KEY_IMAGE_URI)
// Do the work
val response = upload(imageUriInput)
// Create the output of the work
val imageResponse = response.body()
val imgLink = imageResponse.data.link
// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val outputData = workDataOf(Constants.KEY_IMAGE_URI to imgLink)
return Result.success(outputData)
} catch (e: Exception) {
return Result.failure()
}
}
fun upload(imageUri: String): Response {
TODO(“Webservice request code here”)
// Webservice request code here; note this would need to be run
// synchronously for reasons explained below.
}
}
复制代码
有两点需要注意:
Data
传递,它本质上是原始类型和数组的映射。Data
对象应该相当小 —— 实际上可以输入/输出的总大小有限制。这由 MAX_DATA_BYTES
设置。如果您需要将更多数据传入和传出 Worker
,则应将数据放在其他地方,例如 Room database。作为一个例子,我传入上面图像的 URI,而不是图像本身。Result.success()
和 Result.failure()
。还有一个 Result.retry()
选项,它将在之后的时间再次重试你的工作。一方面 Worker
定义工作的作用,另一方面 WorkRequest
定义应该如何以及何时运行工作。
以下是为 UploadWorker
创建 OneTimeWorkRequest
的示例。也可以有重复性的 PeriodicWorkRequest
:
// 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()
复制代码
此 WorkRequest
将输入 imageData: Data
对象,并尽快运行。
假设 UploadWork
并不总是应该立即运行 —— 它应该只在设备有网络连接时运行。你可以通过添加 Constraints
对象来完成此操作。你可以创建这样的约束:
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
复制代码
以下是其他受支持约束的示例:
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.setRequiresStorageNotLow(true)
.setRequiresDeviceIdle(true)
.build()
复制代码
最后,还记得 Result.retry()
吗?我之前说过,如果 Worker
返回 Result.retry()
,WorkManager 将重新计划工作。你可以在创建新的 WorkRequest
时自定义退避条件。这允许你定义何时应重试运行。
退避条件由两个属性定义:
用于对上传工作进行排队的组合代码如下,包括约束,输入和自定义退避策略:
// Create the Constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// Define the input
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
// Bring it all together by creating the WorkRequest; this also sets the back off criteria
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(imageData)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
复制代码
这些都很好,但你还没有真正调度好你的工作去运行。以下是告诉 WorkManager 调度工作所需的一行代码:
WorkManager.getInstance().enqueue(uploadWorkRequest)
复制代码
你首先需要获取 WorkManager
的实例,这是一个负责执行你的工作的单例。调用 enqueue
来启动 WorkManager
跟踪和调度工作的整个过程。
那么,WorkManager
能为您做些什么呢?默认情况下,WorkManager
会:
Worker
类,如上面的 UploadWorker
所示)。让我们探讨一下 WorkManager 如何确保你的工作脱离主线程运行并保证执行。在幕后,WorkManager 包括以下部分:
Executor
,处理所有排队工作的请求。如果您不熟悉 Executors
,可以在这里阅读更多相关信息。
Worker
的实例。我们将在以后的博文中介绍为什么以及如何配置它。
来自:Working with WorkManager Android 开发者大会展示 2018
当你安排 WorkRequest
:
WorkRequest
信息保存到 WorkManager 数据库。WorkRequest
的 Constraints
时(可以立即发生),Internal TaskExecutor 会告诉 WorkerFactory
创建一个 Worker
。Executor
调用你的 Worker
的 doWork()
方法脱离主线程。通过这种方式,默认情况下,你的工作都可以保证执行脱离主线程运行。
现在,如果你想使用除默认 Executor
之外的一些其他机制来运行你的工作,也是可以的!对协程(CoroutineWorker
)和 RxJava(RxWorker
)的开箱即用支持作为工作的手段。
或者,你可以使用 ListenableWorker
准确指定工作的执行方式。Worker
实际上是 ListenableWorker
的一个实现,它默认在默认的 Executor
上运行你的工作,因此是同步的。所以,如果你想要完全控制工作的线程策略或异步运行工作,你可以将 ListenableWorker
子类化(具体细节将在后面的文章中讨论)。
WorkManager 虽然将所有工作信息保存到数据库中有些麻烦,但它还是会做,这使得它成了非常适合需要保障执行的任务。这也是使得 WorkManager 轻松应对对于不需要保障且只需要在后台线程上执行的任务的的原因。例如,假设你已经下载了图像,并且希望根据该图像更改 UI 部分的颜色。这是应该脱离主线程运行的工作,但是,因为它与 UI 直接相关,所以如果关闭应用程序则不需要继续。所以在这样的情况下,不要使用 WorkManager —— 坚持使用像 Kotlin 协程那样轻量的东西或创建自己的 Executor
。
我们的滤镜示例包含的不仅仅是一个任务 —— 我们想要给多个图像加滤镜,然后压缩并上传。如果要一个接一个地或并行地运行一系列 WorkRequests
,则可以使用 链。示例图显示了一个链,其中有三个并行运行的滤镜任务,后面是压缩任务和上传任务,按顺序运行:
使用 WorkManager 非常简单。假设你已经用适当的约束创建了所有 WorkRequests,代码如下所示:
WorkManager.getInstance()
.beginWith(Arrays.asList(
filterImageOneWorkRequest,
filterImageTwoWorkRequest,
filterImageThreeWorkRequest))
.then(compressWorkRequest)
.then(uploadWorkRequest)
.enqueue()
复制代码
三个图片滤镜 WorkRequests
并行执行。一旦完成所有滤镜 WorkRequests (并且只有完成所有三个),就会发生 compressWorkRequest
,然后是 uploadWorkRequest
。
链的另一个优点是:一个 WorkRequest
的输出作为下一个 WorkRequest
的输入。因此,假设你正确设置了输入和输出数据,就像我上面的 UploadWorker
示例所做的那样,这些值将自动传递。
为了处理并行的三个滤镜工作请求的输出,可以使用 InputMerger
,特别是 ArrayCreatingInputMerger
。代码如下:
val compressWorkRequest = OneTimeWorkRequestBuilder<CompressWorker>()
.setInputMerger(ArrayCreatingInputMerger::class.java)
.setConstraints(constraints)
.build()
复制代码
请注意,InputMerger
是添加到 compressWorkRequest
中的,而不是并行的三个滤镜请求中的。
假设每个滤镜工作请求的输出是映射到图像 URI 的键 “KEY_IMAGE_URI”。添加 ArrayCreatingInputMerger
的作用是并行请求的输出,当这些输出具有匹配的键时,它会创建一个包含所有输出值的数组,映射到单个键。可视化图表如下:
ArrayCreatingInputMerger
功能可视化
因此,compressWorkRequest
的输入将最终成为映射到滤镜图像 URI 数组的 “KEY_IMAGE_URI” 对。
监视工作的最简单方法是使用 LiveData
类。如果你不熟悉 LiveData,它是一个生命周期感知的可监视数据持有者 —— 这里 对此有更详细的描述。
调用 getWorkInfoByIdLiveData
返回一个 WorkInfo
的 LiveData
。WorkInfo
包含输出的数据和表示工作状态的枚举。当工作顺利完成后,它的 State
就会是 SUCCEEDED
。因此,例如,你可以通过编写一些监视代码来实现当工作完成时自动显示该图像:
// In your UI (activity, fragment, etc)
WorkManager.getInstance().getWorkInfoByIdLiveData(uploadWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
// Check if the current work's state is "successfully finished"
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
displayImage(workInfo.outputData.getString(KEY_IMAGE_URI))
}
})
复制代码
有几点需要注意:
WorkRequest
都有一个唯一的 id,该唯一 id 是查找关联 WorkInfo
的一种方法。WorkInfo
更改时进行监视并被通知的能力是 LiveData
提供的功能。工作有一个由不同 State
代表的生命周期。监视 LiveData<WorkInfo>
时,你会看到这些状态;例如,你可能会看到:
“happy path” 或工作状态
工作状态经历的 “happy path” 如下:
BLOCKED
:只有当工作在链中并且不是链中的下一个工作时才会出现这种状态。ENQUEUED
:只要工作是工作链中的下一个并且有资格运行,工作就会进入这个状态。这项工作可能仍在等待 Constraint
被满足。RUNNING
:在这种状态时,工作正在运行。对于 Worker
,这意味着 doWork()
方法已经被调用。SUCCEEDED
:当 doWork()
返回 Result.success()
时,工作进入这种最终状态。现在,当工作处于 RUNNING
状态,你可以调用 Result.retry()
。这将会导致工作退回 ENQUEUED
状态。工作也可能随时被 CANCELLED
。
如果工作运行的结果是 Result.failure()
而不是成功。它的状态将会以 FAILED
结束,因此,状态的完整流程图如下所示:
(来自:Working with WorkManager Android 开发者峰会 2018)
想看精彩的视频讲解,请查看 WorkManager Android 开发者峰会演讲。
这就是 WorkManager API 的基础知识。使用我们刚刚介绍的代码片段,你现在就可以:
Worker
。WorkRequest
、Constraint
、启动输入和退出策略配置 Worker
的运行方式。WorkRequest
。WorkManager
在线程和保障运行方面的幕后工作。WorkInfo
监视你的 WorkRequest
的状态。想亲自试试 WorkManager 吗?查看 codelab,包含 Kotlin 和 Java 代码。
随着我们继续更新本系列,请继续关注有关 WorkManager 主题的更多博客文章。 有什么问题或者你希望我们写到的东西吗?请在评论区告诉我们!
感谢 Pietro Maggi