在这篇博文中,我将介绍:
- 将你的后台任务定义为工作
- 定义特定的工作应该如何运行
- 运行你的工作
- 使用链进行存在依赖的工作
- 监视你的工作的状态
我还将解释 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”。
定义你的 work 做什么
在我们将多个任务连接在一起之前,让我们关注如何执行一项工作。我将会着重细说上传任务。首先,你需要创建自己的 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()
选项,它将在之后的时间再次重试你的工作。
定义您的 work 应该如何运行
一方面 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()
.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
时自定义退避条件。这允许你定义何时应重试运行。
退避条件由两个属性定义:
- BackoffPolicy,默认为指数性的,但是可以设置为线性。
- 持续时间,默认为 30 秒。
用于对上传工作进行排队的组合代码如下,包括约束,输入和自定义退避策略:
// 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()
.setInputData(imageData)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
运行 work
这些都很好,但你还没有真正调度好你的工作去运行。以下是告诉 WorkManager 调度工作所需的一行代码:
WorkManager.getInstance().enqueue(uploadWorkRequest)
你首先需要获取 WorkManager
的实例,这是一个负责执行你的工作的单例。调用 enqueue
来启动 WorkManager
跟踪和调度工作的整个过程。
在幕后 —— 工作是怎么运行的
那么,WorkManager
能为您做些什么呢?默认情况下,WorkManager
会:
- 脱离主线程运行你的工作(假设你正在继承
Worker
类,如上面的UploadWorker
所示)。 - 保障 你的工作将会运行(即使你重启设备或应用程序退出,它也不会忘记运行你的工作)。
- 根据用户 API 级别的最佳实践运行(如上一篇文章所述)。
让我们探讨一下 WorkManager 如何确保你的工作脱离主线程运行并保证执行。在幕后,WorkManager 包括以下部分:
-
内部 TaskExecutor:一个单线程
Executor
,处理所有排队工作的请求。如果您不熟悉Executors
,可以在这里阅读更多相关信息。 -
WorkManager 数据库:一个本地数据库,可跟踪所有工作的所有信息和状态。这包括工作的当前状态,工作的输入和输出以及对工作的任何约束限制。此数据库使 WorkManager 能够保证你的工作能够完成 —— 如果你的用户的设备重新启动并且工作中断,则可以从数据库中提取工作的所有详细信息,并在设备再次启动时重新启动工作。
-
WorkerFactory:一个默认工厂,用于创建
Worker
的实例。我们将在以后的博文中介绍为什么以及如何配置它。 -
Default Executor:一个默认的执行程序,运行你的工作,除非你另行指定。这确保在默认情况下,你的工作是同步运行的,并且在主线程之外运行。
-
这些部分可以被重写以具有不同的行为。
来自:Working with WorkManager Android 开发者大会展示 2018
当你安排 WorkRequest
:
- 内部 TaskExecutor 立即将你的
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()
.setInputMerger(ArrayCreatingInputMerger::class.java)
.setConstraints(constraints)
.build()
InputMerger
,特别是 ArrayCreatingInputMerger
。代码如下:
val compressWorkRequest = OneTimeWorkRequestBuilder()
.setInputMerger(ArrayCreatingInputMerger::class.java)
.setConstraints(constraints)
.build()