Android Jetpack系列之 使用 WorkManager 调度任务

1,219 阅读29分钟

Android Jetpack系列之 使用 WorkManager 调度任务

原文 WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是永久性的工作。 由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。

1.持久性工作的类型

WorkManager 可处理三种类型的永久性工作: 1.立即执行:必须立即开始且很快就完成的任务,可以加急。 2.长时间运行:运行时间可能较长(有可能超过 10 分钟)的任务。 3.可延期执行:延期开始并且可以定期运行的预定任务。

图 1 大致表明了不同类型的持久性工作彼此之间的关系。

详见原文

2.功能

除了具备更为简单且一致的 API 之外,WorkManager 还具备许多其他关键优势:

2.1 工作约束

使用工作约束明确定义工作运行的最佳条件。例如,仅在设备采用不按流量计费的网络连接时、当设备处于空闲状态或者有足够的电量时运行。

2.2 强大的调度

WorkManager 允许您使用灵活的调度窗口调度工作,以运行一次性重复工作。您还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。

已调度的工作存储在内部托管的 SQLite 数据库中,由 WorkManager 负责确保该工作持续进行,并在设备重新启动后重新调度。

此外,WorkManager 遵循低电耗模式等省电功能和最佳做法,因此您在这方面无需担心。

2.3加急工作

您可以使用 WorkManager 调度需在后台立即执行的工作。您应该使用加急工作来处理对用户来说很重要且会在几分钟内完成的任务。

2.4灵活的重试政策

有时工作会失败。WorkManager 提供了灵活的重试政策,包括可配置的指数退避政策

2.5工作链

对于复杂的相关工作,您可以使用直观的接口将各个工作任务串联起来,这样您便可以控制哪些部分依序运行,哪些部分并行运行。

val continuation = WorkManager.getInstance(context)
    .beginUniqueWork(
        Constants.IMAGE_MANIPULATION_WORK_NAME,
        ExistingWorkPolicy.REPLACE,
        OneTimeWorkRequest.from(CleanupWorker::class.java)
    ).then(OneTimeWorkRequest.from(WaterColorFilterWorker::class.java))
    .then(OneTimeWorkRequest.from(GrayScaleFilterWorker::class.java))
    .then(OneTimeWorkRequest.from(BlurEffectFilterWorker::class.java))
    .then(
        if (save) {
            workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
        } else /* upload */ {
            workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
        }
    )

对于每项工作任务,您可以定义工作的输入和输出数据。将工作串联在一起时,WorkManager 会自动将输出数据从一个工作任务传递给下一个工作任务。

2.6 内置线程互操作性

WorkManager 无缝集成 CoroutinesRxJava,让您可以插入自己的异步 API,非常灵活。

注意:尽管 Coroutines 和 WorkManager 分别是针对于不同用例推荐的解决方案,但它们二者并不是互斥的关系。您可以在通过 WorkManager 调度的工作中使用协程。

3.使用 WorkManager 保证工作可靠性

WorkManager 适用于需要可靠运行的工作,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。例如:

  • 向后端服务发送日志或分析数据。
  • 定期将应用数据与服务器同步。

WorkManager 不适用于那些可在应用进程结束时安全终止的进程内后台工作。它也并非对所有需要立即执行的工作都适用的通用解决方案。请查看后台处理指南,了解哪种解决方案符合您的需求。

4.与其他 API 的关系

虽然协程是适合某些用例的推荐解决方案,但您不应将其用于持久性工作。请务必注意,协程是一个并发框架,而 WorkManager 是一个持久性工作库。同样,AlarmManager 仅适合用于时钟或日历。

API推荐使用场景与 WorkManager 的关系
Coroutines所有不需要持久的异步工作。协程是在 Kotlin 中退出主线程的标准方式。不过,它们在应用关闭后会释放内存。对于持久性工作,请使用 WorkManager。
AlarmManager仅限闹钟。与 WorkManager 不同,AlarmManager 会使设备从低电耗模式中唤醒。因此,它在电源和资源管理方面来讲并不高效。AlarmManager 仅适合用于精确闹钟或通知(例如日历活动)场景,而不适用于后台工作。

5.取代已弃用的 API

WorkManager API 是一个适合用来替换先前的 Android 后台调度 API(包括 FirebaseJobDispatcherGcmNetworkManagerJobScheduler)的推荐组件。

注意:如果您的应用以 Android 10(API 级别 29)或更高版本为目标平台,那么对 yourFirebaseJobDispatcherGcmNetworkManager API 的调用在搭载 Android Marshmallow (6.0) 及更高版本的设备上将无法正常工作。如需迁移指导,请参阅 FirebaseJobDispatcherGcmNetworkManager 迁移指南。此外,请参阅统一 Android 上的后台任务调度公告,详细了解与弃用这些 API 相关的信息。

6.使用入门

6.1 导入依赖

如需开始使用 WorkManager,请先将库导入您的 Android 项目中。

将以下依赖项添加到应用的 build.gradle 文件中:

dependencies {
    val work_version = "2.7.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")

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

6.2定义工作

工作使用 Worker 类定义。doWork() 方法在 WorkManager 提供的后台线程上异步运行。

如需为 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 work finished successfully with the Result
       return Result.success()
   }
}

doWork() 返回的 Result 会通知 WorkManager 服务工作是否成功,以及工作失败时是否应重试工作。

  • Result.success():工作成功完成。
  • Result.failure():工作失败。
  • Result.retry():工作失败,应根据其重试政策在其他时间尝试。

6.3 创建 WorkRequest

定义工作后,必须使用 WorkManager 服务进行调度该工作才能运行。对于如何调度工作,WorkManager 提供了很大的灵活性。您可以将其安排为在某段时间内定期运行,也可以将其安排为仅运行一次

不论您选择以何种方式调度工作,请始终使用 WorkRequest.Worker 定义工作单元,WorkRequest(及其子类)则定义工作运行方式和时间。在最简单的情况下,您可以使用 OneTimeWorkRequest,如以下示例所示。

val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<UploadWorker>()
       .build()

6.4将 WorkRequest 提交给系统

最后,您需要使用 enqueue() 方法将 WorkRequest 提交到 WorkManager

WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest)

执行工作器的确切时间取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供最佳行为。

本入门指南只介绍了一些简单信息。WorkRequest 中还可以包含其他信息,例如在运行工作时应遵循的约束、工作输入、延迟,以及重试工作的退避时间政策。下一部分定义工作请求将更详细地介绍这些选项,以及如何调度唯一工作和重复性工作。

7 定义工作请求

入门指南介绍了如何创建简单的 WorkRequest 并将其加入队列。

在本指南中,您将了解如何定义和自定义 WorkRequest 对象来处理常见用例,例如:

  • 调度一次性工作和重复性工作
  • 设置工作约束条件,例如要求连接到 Wi-Fi 网络或正在充电
  • 确保至少延迟一定时间再执行工作
  • 设置重试和退避策略
  • 将输入数据传递给工作
  • 使用标记将相关工作分组在一起

7.1 定义WorkRequest

val myWorkRequest = ...
WorkManager.getInstance(myContext).enqueue(myWorkRequest)

WorkRequest 对象包含 WorkManager 调度和运行工作所需的所有信息。其中包括运行工作必须满足的约束、调度信息(例如延迟或重复间隔)、重试配置,并且可能包含输入数据(如果工作需要)。

WorkRequest 本身是抽象基类。该类有两个派生实现,可用于创建 OneTimeWorkRequestPeriodicWorkRequest 请求。顾名思义,OneTimeWorkRequest 适用于调度非重复性工作,而 PeriodicWorkRequest 则更适合调度以一定间隔重复执行的工作。

7.2 调度一次性工作

对于无需额外配置的简单工作,请使用静态方法 from

val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)

对于更复杂的工作,可以使用构建器:

val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       // Additional configuration
       .build()

7.3调度加急工作

WorkManager 2.7.0 引入了加急工作的概念。这使 WorkManager 能够执行重要工作,同时使系统能够更好地控制对资源的访问权限。

加急工作具有以下特征:

  • 重要性:加急工作适用于对用户很重要或由用户启动的任务。
  • 速度:加急工作最适合那些立即启动并在几分钟内完成的简短任务。
  • 配额:限制前台执行时间的系统级配额决定了加急作业是否可以启动。
  • 电源管理电源管理限制(如省电模式和低电耗模式)不太可能影响加急工作。
  • 延迟时间:系统立即执行加急工作,前提是系统的当前工作负载允许执行此操作。这意味着这些工作对延迟时间较为敏感,不能安排到以后执行。

在用户想要发送消息或附加的图片时,可能会在聊天应用内使用加急工作。同样,处理付款或订阅流程的应用也可能需要使用加急工作。这是因为这些任务对用户很重要,会在后台快速执行,并需要立即开始执行。

7.3.1配额

系统必须先为加急作业分配应用执行时间,然后才能运行作业。执行时间并非无限制,而是受配额限制。如果您的应用使用其执行时间并达到分配的配额,在配额刷新之前,您无法再执行加急工作。这样,Android 可以更有效地在应用之间平衡资源。

每个应用均有自己的前台执行时间配额。可用的执行时间取决于待机模式存储分区和进程的重要性。

您可以确定在执行配额不允许立即运行加急作业时会出现什么情况。如需了解详情,请参阅以下代码段。

注意:当您的应用在前台运行时,配额不会限制加急工作的执行。仅在应用在后台运行时或当应用移至后台时,执行时间配额才适用。因此,您应在后台加快要继续的工作。当应用在前台运行时,您可以继续使用 setForeground()

7.4执行加急工作

从 WorkManager 2.7 开始,您的应用可以调用 setExpedited() 来声明 WorkRequest 应该使用加急作业,以尽可能快的速度运行。以下代码段展示了关于如何使用 setExpedited() 的示例:

val request = OneTimeWorkRequestBuilder()
	 //找不到这一个api
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(context)
    .enqueue(request)

在此示例中,我们初始化 OneTimeWorkRequest 的实例并对其调用 setExpedited()。然后,此请求就会变成加急工作。如果配额允许,它将立即开始在后台运行。

加急任务受基于 应用待机群组 (App Standby Buckets) 的配额限制,当您的应用尝试在超出配额的情况下执行加急任务时,WorkManager 会根据 OutOfQuotaPolicy 参数做出相应的行为: 完全放弃加急任务请求 (DROP_WORK_REQUEST),或者将该加急任务降级至普通任务 (RUN_AS_NON_EXPEDITED_WORK_REQUEST)。加急任务很重要,但并不意味着它可以一直执行,您需要将配额视为执行加急任务的时间限制。

7.4.1 向后兼容性和前台服务

为了保持加急作业的向后兼容性,WorkManager 可能会在 Android 12 之前版本的平台上运行前台服务。前台服务可以向用户显示通知。

在 Android 12 之前,工作器中的 getForegroundInfoAsync()getForegroundInfo() 方法可让 WorkManager 在您调用 setExpedited() 时显示通知。

如果您想要请求任务作为加急作业运行,则所有的 ListenableWorker 都必须实现 getForegroundInfo 方法。

注意:如果未能实现对应的 getForegroundInfo 方法,那么在旧版平台上调用 setExpedited 时,可能会导致运行时崩溃。

以 Android 12 或更高版本为目标平台时,前台服务仍然可通过对应的 setForeground 方法使用。

注意setForeground() 可能会在 Android 12 上抛出运行时异常,并且在启动受到限制时可能会抛出异常。

class ExpeditedWorker(appContext: Context, workerParams: WorkerParameters) :
    CoroutineWorker(appContext, workerParams) {
    override suspend fun doWork(): Result {
        delay(2000L)
        return Result.success()
    }

    override suspend fun getForegroundInfo(): ForegroundInfo {
        val notificationManager =
            applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "NOTIFICATION_CHANNEL_ID",
                "NOTIFICATION_CHANNEL_NAME",
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationManager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(applicationContext, "NOTIFICATION_CHANNEL_ID")
            .setContentIntent(
                PendingIntent.getActivity(
                    applicationContext, 0,
                    Intent(applicationContext, MainActivity::class.java),
                    Intent.FILL_IN_PACKAGE
                ),
            )
            .setSmallIcon(com.google.android.material.R.drawable.mtrl_ic_error)
            .setOngoing(true)
            .setAutoCancel(true)
            .setOnlyAlertOnce(true)
            .setPriority(NotificationCompat.PRIORITY_MIN)
            .setContentTitle(applicationContext.getString(R.string.app_name))
            .setLocalOnly(true)
            .setVisibility(NotificationCompat.VISIBILITY_SECRET)
            .setContentText("Updating widget")
            .build()
        return ForegroundInfo(1337, notification)
    }
}
val request = OneTimeWorkRequestBuilder<ExpeditedWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(this)
    .enqueue(request)
7.4.2 工作器

工作器不知道自身所执行的工作是否已加急。不过,在某些版本的 Android 上,如果 WorkRequest 被加急,工作器可以显示通知。

为此,WorkManager 提供了 getForegroundInfoAsync() 方法,您必须实现该方法,让 WorkManager 在必要时显示通知,以便启动 ForegroundService

7.4.3 CoroutineWorker

如果您使用 CoroutineWorker,则必须实现 getForegroundInfo()。然后,在 doWork() 内将其传递给 setForeground()。这样做会在 Android 12 之前的版本中创建通知。

请参考以下示例:

  class ExpeditedWorker(appContext: Context, workerParams: WorkerParameters):
   CoroutineWorker(appContext, workerParams) {

   override suspend fun getForegroundInfo(): ForegroundInfo {
       return ForegroundInfo(
           NOTIFICATION_ID, createNotification()
       )
   }

   override suspend fun doWork(): Result {
       TODO()
   }

    private fun createNotification() : Notification {
       TODO()
    }

}

注意:您应该将 setForeground() 封装在 try/catch 块中,以捕获可能出现的 IllegalStateException。如果您的应用此时无法在前台运行,便可能会发生这类异常。在 Android 12 及更高版本中,您可以使用更详细的 ForegroundServiceStartNotAllowedException

7.4.4 配额政策

您可以控制当应用达到其执行配额时加急工作会发生什么情况。如需继续,您可以传递 setExpedited()

  • OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST,这会导致作业作为普通工作请求运行。上述代码段演示了此操作。
  • OutOfQuotaPolicy.DROP_WORK_REQUEST,这会在配额不足时导致请求取消。
7.4.5 示例应用

如需查看关于 WorkManager 2.7.0 如何使用加急工作的完整示例,请查看 GitHub 上的 WorkManagerSample

7.4.6 延迟加急工作

系统会尝试在调用指定的加急作业后,尽快执行该作业。不过,与其他类型的作业一样,系统可能会延迟启动新的加急工作,如在以下情况下:

  • 负载:系统负载过高,当有过多作业已在运行或者当系统内存不足时,就会发生这种情况。
  • 配额:已超出加急作业配额限制。加急工作使用基于应用待机存储分区的配额系统,并限制滚动时间窗口中的最大执行时间。用于加急工作的配额比用于其他类型的后台作业的配额限制性更强。

7.5 调度定期工作

您的应用有时可能需要定期运行某些工作。例如,您可能要定期备份数据、定期下载应用中的新鲜内容或者定期上传日志到服务器。

使用 PeriodicWorkRequest 创建定期执行的 WorkRequest 对象的方法如下:

val saveRequest =
       PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
    // Additional configuration
           .build()

在此示例中,工作的运行时间间隔定为一小时。

时间间隔定义为两次重复执行之间的最短时间。工作器的确切执行时间取决于您在 WorkRequest 对象中设置的约束以及系统执行的优化。

注意:可以定义的最短重复间隔是 15 分钟(与 JobScheduler API 相同)。

7.5.1 灵活的运行间隔

如果您的工作的性质致使其对运行时间敏感,您可以将 PeriodicWorkRequest 配置为在每个时间间隔的灵活时间段内运行,如图 1 所示。

原图请查看原文

图 1. 此图显示了可在灵活时间段内运行工作的的重复间隔。

如需定义具有灵活时间段的定期工作,请在创建 PeriodicWorkRequest 时传递 flexInterval 以及 repeatInterval。灵活时间段从 repeatInterval - flexInterval 开始,一直到间隔结束。

以下是可在每小时的最后 15 分钟内运行的定期工作的示例。

val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
       1, TimeUnit.HOURS, // repeatInterval (the period cycle)
       15, TimeUnit.MINUTES) // flexInterval
    .build()

重复间隔必须大于或等于 PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS(15分钟),而灵活间隔必须大于或等于 PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS(5分钟)。

7.5.2 约束对定期工作的影响

您可以对定期工作设置约束。例如,您可以为工作请求添加约束,以便工作仅在用户设备充电时运行。在这种情况下,除非满足约束条件,否则即使过了定义的重复间隔,PeriodicWorkRequest 也不会运行。这可能会导致工作在某次运行时出现延迟,甚至会因在相应间隔内未满足条件而被跳过。

7.6工作约束

约束可确保将工作延迟到满足最佳条件时运行。以下约束适用于 WorkManager。

NetworkType约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。
BatteryNotLow如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。
RequiresCharging如果设置为 true,那么工作只能在设备充电时运行。
DeviceIdle如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能。
StorageNotLow如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。

如需创建一组约束并将其与某项工作相关联,请使用一个 Contraints.Builder() 创建 Constraints 实例,并将该实例分配给 WorkRequest.Builder()

例如,以下代码会构建了一个工作请求,该工作请求仅在用户设备正在充电且连接到 Wi-Fi 网络时才会运行:

val constraints = Constraints.Builder()
   .setRequiredNetworkType(NetworkType.UNMETERED)
   .setRequiresCharging(true)
   .build()

val myWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       .setConstraints(constraints)
       .build()

如果指定了多个约束,工作将仅在满足所有约束时才会运行。

如果在工作运行时不再满足某个约束,WorkManager 将停止工作器。系统将在满足所有约束后重试工作。

7.7 延迟工作

如果工作没有约束,或者当工作加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动。

下面举例说明了如何将工作设置为在加入队列后至少经过 10 分钟后再运行。

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setInitialDelay(10, TimeUnit.MINUTES)
   .build()

该示例说明了如何为 OneTimeWorkRequest 设置初始延迟时间,您也可以为 PeriodicWorkRequest 设置初始延迟时间。在这种情况下,定期工作只有【首次】运行时会延迟。

注意:执行工作器的确切时间还取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供可能的最佳行为。

7.8重试和退避政策

如果您需要让 WorkManager 重试工作,可以从工作器返回 Result.retry()。然后,系统将根据退避延迟时间退避政策重新调度工作。

  • 退避延迟时间指定了首次尝试后重试工作前的最短等待时间。此值不能超过 10 秒(或 MIN_BACKOFF_MILLIS)。
  • 退避政策定义了在后续重试过程中,退避延迟时间随时间以怎样的方式增长。WorkManager 支持 2 个退避政策,即 LINEAREXPONENTIAL

每个工作请求都有退避政策和退避延迟时间。默认政策是 EXPONENTIAL,延迟时间为 10 秒,但您可以在工作请求配置中替换此设置。

以下是自定义退避延迟时间和政策的示例。

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
       TimeUnit.MILLISECONDS)
   .build()

在本示例中,最短退避延迟时间设置为允许的最小值,即 10 秒。由于政策为 LINEAR,每次尝试重试时,重试间隔都会增加约 10 秒。例如,第一次运行以 Result.retry() 结束并在 10 秒后重试;然后,如果工作在后续尝试后继续返回 Result.retry(),那么接下来会在 20 秒、30 秒、40 秒后重试,以此类推。如果退避政策设置为 EXPONENTIAL,那么重试时长序列将接近 20、40、80 秒,以此类推。

注意:退避延迟时间不精确,在两次重试之间可能会有几秒钟的差异,但绝不会低于配置中指定的初始退避延迟时间。

7.9 标记工作

每个工作请求都有一个唯一标识符,该标识符可用于在以后标识该工作,以便取消工作或观察其进度

如果有一组在逻辑上相关的工作,对这些工作项进行标记可能也会很有帮助。通过标记,您一起处理一组工作请求。

例如,WorkManager.cancelAllWorkByTag(String) 会取消带有特定标记的所有工作请求,WorkManager.getWorkInfosByTag(String) 会返回一个 WorkInfo 对象列表,该列表可用于确定当前工作状态。

以下代码展示了如何向工作添加“cleanup”标记:

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
	//默认本身就会有一个tags,就是类的全称字符串
   .addTag("cleanup")
   .build()
   val isDone = WorkManager.getInstance(this@TestWorkManagerActivity).getWorkInfosByTag("cleanup").isDone

最后,可以向单个工作请求添加多个标记。这些标记在内部以一组字符串的形式进行存储。您可以使用 WorkInfo.getTags() 获取与 WorkRequest 关联的标记集。

Worker 类中,您可以通过 ListenableWorker.getTags() 检索其标记集。

7.10 分配输入数据

您的工作可能需要输入数据才能正常运行。例如,处理图片上传的工作可能需要使用待上传图片的 URI 作为输入数据。

输入值以键值对的形式存储在 Data 对象中,并且可以在工作请求中设置。WorkManager 会在执行工作时将输入 Data 传递给工作。Worker 类可通过调用 Worker.getInputData() 访问输入参数。以下代码展示了如何创建需要输入数据的 Worker 实例,以及如何在工作请求中发送该实例。

// Define the Worker requiring input
class UploadWork(appContext: Context, workerParams: WorkerParameters)
   : Worker(appContext, workerParams) {

   override fun doWork(): Result {
       val imageUriInput =
           inputData.getString("IMAGE_URI") ?: return Result.failure()

       uploadFile(imageUriInput)
       return Result.success()
   }
   ...
}

// Create a WorkRequest for your Worker and sending it input
val myUploadWork = OneTimeWorkRequestBuilder<UploadWork>()
   .setInputData(workDataOf(
       "IMAGE_URI" to "http://..."
   ))
   .build()

同样,可使用 Data 类输出返回值。如需详细了解输入和输出数据,请参阅输入参数和返回值部分。

8.工作状态

工作在其整个生命周期内经历了一系列 State 更改。

8.1 一次性工作的状态

对于 one-time 工作请求,工作的初始状态为 ENQUEUED。(队列,入队)

ENQUEUED 状态下,您的工作会在满足其 Constraints (约束)和初始延迟计时要求后立即运行。接下来,该工作会转为 RUNNING 状态,然后可能会根据工作的结果转为 SUCCEEDEDFAILED 状态;或者,如果结果是 retry,它可能会回到 ENQUEUED 状态。在此过程中,随时都可以取消工作,取消后工作将进入 CANCELLED 状态。

图 1 展示了一次性工作的生命周期,事件可能会进入另一个状态。

原图请查看原文

SUCCEEDEDFAILEDCANCELLED 均表示此工作的终止状态。如果您的工作处于上述任何状态,WorkInfo.State.isFinished() 都将返回 true。

8.2 定期工作的状态

成功和失败状态仅适用于一次性工作和链式工作定期工作只有一个终止状态 CANCELLED。这是因为定期工作永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。图 2 描述了定期工作的精简状态图。

原图请查看原文

8.3 BLOCKED 状态

还有一种我们尚未提到的最终状态,那就是 BLOCKED。此状态适用于一系列已编排的工作,或者说工作链。链接工作中介绍了工作链及其状态图。

9.管理工作

唯一工作,查队表,查状态(观察),取消停止

定义 WorkerWorkRequest 后,最后一步是将工作加入队列。将工作加入队列的最简单方法是调用 WorkManager enqueue() 方法,然后传递要运行的 WorkRequest

val myWork: WorkRequest = // ... OneTime or PeriodicWork
WorkManager.getInstance(requireContext()).enqueue(myWork)

在将工作加入队列时请小心谨慎,以避免重复。例如,应用可能会每 24 小时尝试将其日志上传到后端服务。如果不谨慎,即使作业只需运行一次,您最终也可能会多次将同一作业加入队列。为了实现此目标,您可以将工作调度为唯一工作

9.1 唯一工作

唯一工作是一个很实用的概念,可确保同一时刻只有一个具有特定名称的工作实例。与 ID 不同的是,唯一名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,唯一名称仅与一个工作实例相关联。

唯一工作既可用于一次性工作,也可用于定期工作。您可以通过调用以下方法之一创建唯一工作序列,具体取决于您是调度重复工作还是一次性工作。

这两种方法都接受 3 个参数:

  • uniqueWorkName - 用于唯一标识工作请求的 String
  • existingWorkPolicy - 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作。如需了解详情,请参阅冲突解决政策
  • work - 要调度的 WorkRequest

借助唯一工作,我们可以解决前面提到的重复调度问题。

val sendLogsWorkRequest =
       PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)
           .setConstraints(Constraints.Builder()
               .setRequiresCharging(true)
               .build()
            )
           .build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
           "sendLogs",
           ExistingPeriodicWorkPolicy.KEEP,
           sendLogsWorkRequest
)

现在,如果上述代码在 sendLogs 作业已处于队列中的情况下运行,系统会保留现有的作业,并且不会添加新的作业。

当您需要逐步构建一个长任务链时,也可以利用唯一工作序列。例如,照片编辑应用可能允许用户撤消一长串操作。其中的每一项撤消操作可能都需要一些时间来完成,但必须按正确的顺序执行。在这种情况下,应用可以创建一个“撤消”链,并根据需要将每个撤消操作附加到该链上。如需了解详情,请参阅链接工作

9.2 冲突解决政策

调度唯一工作时,您必须告知 WorkManager 在发生冲突时要执行的操作。您可以通过在将工作加入队列时传递一个枚举来实现此目的。

对于一次性工作,您需要提供一个 ExistingWorkPolicy,它支持用于处理冲突的 4 个选项。

  • REPLACE:用新工作替换现有工作。此选项将取消现有工作。
  • KEEP:保留现有工作,并忽略新工作。
  • APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。

现有工作将成为新工作的先决条件。如果现有工作变为 CANCELLEDFAILED 状态,新工作也会变为 CANCELLEDFAILED。如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE

  • APPEND_OR_REPLACE 函数类似于 APPEND,不过它并不依赖于先决条件工作状态。即使现有工作变为 CANCELLEDFAILED 状态,新工作仍会运行。

对于定期工作,您需要提供一个 ExistingPeriodicWorkPolicy,它支持 REPLACEKEEP 这两个选项。这些选项的功能与其对应的 ExistingWorkPolicy 功能相同。

9.3 观察您的工作

在将工作加入队列后,您可以随时按其 nameid 或与其关联的 tag 在 WorkManager 中进行查询,以检查其状态。

// by id
workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>

// by name
workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>

// by tag
workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>

该查询会返回 WorkInfo 对象的 ListenableFuture,该值包含工作的 id、其标记、其当前的 State 以及通过 Result.success(outputData) 设置的任何输出数据。

利用每个方法的 LiveData 变种,您可以通过注册监听器来观察 WorkInfo 的变化。例如,如果您想要在某项工作成功完成后向用户显示消息,您可以进行如下设置:

workManager.getWorkInfoByIdLiveData(syncWorker.id)
               .observe(viewLifecycleOwner) { workInfo ->
   if(workInfo?.state == WorkInfo.State.SUCCEEDED) {
       Snackbar.make(requireView(),
      R.string.work_completed, Snackbar.LENGTH_SHORT)
           .show()
   }
}

//或者
 WorkManager.getInstance(this@TestWorkManagerActivity)
     .enqueue(uploadWorkRequest)
     .state.observe(this@TestWorkManagerActivity, Observer {
         //状态改变时回调
         LogUtils.d("state.observe:$it")//state.observe:SUCCESS
     })
                    

9.4 复杂的工作[查询]Query

WorkManager 2.4.0 及更高版本支持使用 WorkQuery 对象对已加入队列的作业进行复杂查询。WorkQuery 支持按工作的标记、状态和唯一工作名称的组合进行查询。

以下示例说明了如何查找带有“syncTag”标记、处于 FAILEDCANCELLED 状态,且唯一工作名称为“preProcess”或“sync”的所有工作。

val workQuery = WorkQuery.Builder
       .fromTags(listOf("syncTag"))
       .addStates(listOf(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
       .addUniqueWorkNames(listOf("preProcess", "sync")
    )
   .build()

val workInfos: ListenableFuture<List<WorkInfo>> = workManager.getWorkInfos(workQuery)
WorkQuery` 中的每个组件(标记、状态或名称)与其他组件都是 `AND` 逻辑关系。组件中的每个值都是 `OR` 逻辑关系。例如:`<em>(name1 OR name2 OR ...) AND (tag1 OR tag2 OR ...) AND (state1 OR state2 OR ...)</em>

WorkQuery 也适用于等效的 LiveData 方法 getWorkInfosLiveData()

9.5取消和停止工作

如果您不再需要运行先前加入队列的工作,则可以要求将其取消。您可以按工作的 nameid 或与其关联的 tag 取消工作。

// by id
workManager.cancelWorkById(syncWorker.id)

// by name
workManager.cancelUniqueWork("sync")

// by tag
workManager.cancelAllWorkByTag("syncTag")

WorkManager 会在后台检查工作的 State。如果工作已经完成,系统不会执行任何操作。否则,工作的状态会更改为 CANCELLED,之后就不会运行这个工作。任何依赖于此工作WorkRequest 作业也将变为 CANCELLED

目前,RUNNING 可收到对 ListenableWorker.onStopped() 的调用。如需执行任何清理操作,请替换此方法。如需了解详情,请参阅停止正在运行的工作器

注意cancelAllWorkByTag(String) 会取消具有给定标记的所有工作。

9.6停止正在运行的工作器

正在运行的 Worker 可能会由于以下几种原因而停止运行:

  • 您明确要求取消它(例如,通过调用 WorkManager.cancelWorkById(UUID) 取消)。
  • 如果是唯一工作,您明确地将 ExistingWorkPolicyREPLACE 的新 WorkRequest 加入到了队列中。旧的 WorkRequest 会立即被视为已取消。
  • 您的工作约束条件已不再满足。
  • 系统出于某种原因指示您的应用停止工作。如果超过 10 分钟的执行期限,可能会发生这种情况。该工作会调度为在稍后重试。

在这些情况下,您的工作器会停止。

您应该合作地取消正在进行的任何工作,并释放您的工作器保留的所有资源。例如,此时应该关闭所打开的数据库和文件句柄。有两种机制可让您了解工作器何时停止。

9.6.1 onStopped() 回调

在您的工作器停止后,WorkManager 会立即调用 ListenableWorker.onStopped()。替换此方法可关闭您可能保留的所有资源。

9.6.2isStopped() 属性

您可以调用 ListenableWorker.isStopped() 方法以检查工作器是否已停止。如果您在工作器执行长时间运行的操作或重复操作,您应经常检查此属性,并尽快将其用作停止工作的信号。

注意:WorkManager 会忽略已收到 onStop 信号的工作器所设置的 Result,因为工作器已被视为停止。

只有显示取消或其他条件的才会从队列移除?

10.观察工作器的中间进度

WorkManager 2.3.0-alpha01 为设置和观察工作器的中间进度添加了一流的支持。如果应用在前台运行时,工作器保持运行状态,那么也可以使用返回 WorkInfoLiveData 的 API 向用户显示此信息。

ListenableWorker 现在支持 setProgressAsync() API,此类 API 允许保留中间进度。借助这些 API,开发者能够设置可通过界面观察到的中间进度。进度由 Data 类型表示,这是一个可序列化的属性容器(类似于 inputoutput,并且受到相同的限制)。

只有在 ListenableWorker 运行时才能观察到和更新进度信息。如果尝试在 ListenableWorker 完成执行后在其中设置进度,则将会被忽略。您还可以使用 getWorkInfoBy…()getWorkInfoBy…LiveData() 方法来观察进度信息。这两个方法会返回 WorkInfo 的实例,后者有一个返回 Data 的新 getProgress() 方法。

10.1 更新进度

对于使用 ListenableWorkerWorker 的 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()
    }
}

10.2 观察进度

观察进度信息也很简单。您可以使用 getWorkInfoBy…()getWorkInfoBy…LiveData() 方法,并引用 WorkInfo

以下是使用 getWorkInfoByIdLiveData API 的示例。

WorkManager.getInstance(applicationContext)
    // requestId is the WorkRequest id
    .getWorkInfoByIdLiveData(requestId)
    //LiveData监听是有生命周期感知的,但work是没有,要手动取消,否则会一直执行到结束
    .observe(observer, Observer { workInfo: WorkInfo? ->
            if (workInfo != null) {
                val progress = workInfo.progress
                val value = progress.getInt(Progress, 0)
                // Do something with progress information
            }
    })

11.链接工作

您可以使用 WorkManager 创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序。当您需要以特定顺序运行多个任务时,此功能尤其有用。

如需创建工作链,您可以使用 WorkManager.beginWith(OneTimeWorkRequest)WorkManager.beginWith(List),这会返回 WorkContinuation 实例。

然后,可以使用 WorkContinuation 通过 then(OneTimeWorkRequest)then(List) 添加 OneTimeWorkRequest 依赖实例。 .

每次调用 WorkContinuation.then(...) 都会返回一个新的 WorkContinuation 实例。如果添加了 OneTimeWorkRequest 实例的 List,这些请求可能会并行运行。

最后,您可以使用 WorkContinuation.enqueue() 方法对 WorkContinuation 工作链执行 enqueue() 操作。

下面我们来看一个示例。在本例中,有 3 个不同的工作器作业配置为运行(可能并行运行)。然后这些工作器的结果将联接起来,并传递给正在缓存的工作器作业。最后,该作业的输出将传递到上传工作器,由上传工作器将结果上传到远程服务器。

WorkManager.getInstance(myContext)
   // Candidates to run in parallel 
   //.beginWith(listOf(plantName1, plantName2, plantName3))是并行处理的。
   .beginWith(listOf(plantName1, plantName2, plantName3))
   // Dependent work (only runs after all previous work in chain) 前面的任务运行完后才执行。
   .then(cache)
   .then(upload)
   // Call enqueue to kick things off
   .enqueue()

11.1 输入合并器

当您链接 OneTimeWorkRequest 实例时,父级工作请求的输出将作为子级的输入传入。因此,在上面的示例中,plantName1plantName2plantName3 的输出将作为 cache 请求的输入传入。

为了管理来自多个父级工作请求的输入,WorkManager 使用 InputMerger

WorkManager 提供两种不同类型的 InputMerger

如果您有更具体的用例,则可以创建 InputMerger 的子类来编写自己的用例。

11.1.1 OverwritingInputMerger

OverwritingInputMerger 是默认的合并方法。如果合并过程中存在键冲突,键的最新值将覆盖生成的输出数据中的所有先前版本。

例如,如果每种植物的输入都有一个与其各自变量名称("plantName1""plantName2""plantName3")匹配的键,传递给 cache 工作器的数据将具有三个键值对。

原图请看原文

如果存在冲突,那么最后一个工作器将在争用中“取胜”,其值将传递给 cache

由于工作请求是并行运行的,因此无法保证其运行顺序。在上面的示例中,plantName1 可以保留值 "tulip""elm",具体取决于最后写入的是哪个值。如果有可能存在键冲突,并且您需要在合并器中保留所有输出数据,那么 ArrayCreatingInputMerger 可能是更好的选择。

11.1.2 ArrayCreatingInputMerger

对于上面的示例,假设我们要保留所有植物名称工作器的输出,则应使用 ArrayCreatingInputMerger

val cache: OneTimeWorkRequest = OneTimeWorkRequestBuilder<PlantWorker>()
   .setInputMerger(ArrayCreatingInputMerger::class)
   .setConstraints(constraints)
   .build()

如果存在任何键冲突,那么所有对应的值会分组到一个数组中。

原图请看原文

11.2 链接和工作状态

只要工作成功完成(即,返回 Result.success()),OneTimeWorkRequest 链便会按顺序执行。运行时,工作请求可能会失败或被取消,这会对依存工作请求产生下游影响。

当第一个 OneTimeWorkRequest 被加入工作请求链队列时,所有后续工作请求会被屏蔽,直到第一个工作请求的工作完成为止。

原图请看原文

在加入队列且满足所有工作约束后,第一个工作请求开始运行。如果工作在根 OneTimeWorkRequestList<OneTimeWorkRequest> 中成功完成(即返回 Result.success()),系统会将下一组依存工作请求加入队列。

原图请看原文

只要每个工作请求都成功完成,工作请求链中的剩余工作请求就会遵循相同的运行模式,直到链中的所有工作都完成为止。这是最简单的用例,通常也是首选用例,但处理错误状态同样重要。

如果在工作器处理工作请求时出现错误,您可以根据您定义的退避政策来重试该请求。重试请求链中的某个请求意味着,系统将使用提供给该请求的输入数据仅对该请求进行重试。并行运行的所有其他作业均不会受到影响。

原图请看原文

如需详细了解如何定义自定义重试策略,请参阅重试和退避政策

如果该重试政策未定义或已用尽,或者您以其他方式已达到 OneTimeWorkRequest 返回 Result.failure() 的某种状态,该工作请求和所有依存工作请求都会被标记为 FAILED.

原图请看原文

OneTimeWorkRequest 被取消时遵循相同的逻辑。任何依存工作请求也会被标记为 CANCELLED,并且无法执行其工作。

原图请看原文

请注意,如果要向已失败或已取消工作请求的链附加更多工作请求,新附加的工作请求也会分别标记为 FAILEDCANCELLED。如果您想扩展现有链的工作,请参阅 ExistingWorkPolicy 中的 APPEND_OR_REPLACE

创建工作请求链时,依存工作请求应定义重试政策,以确保始终及时完成工作。失败的工作请求可能会导致链不完整和/或出现意外状态。

如需了解详情,请参阅取消和停止工作

12.测试 Worker 实现

WorkManager 提供了用于测试 WorkerListenableWorkerListenableWorker 变体(CoroutineWorkerRxWorker)的 API。

注意:在版本 2.1.0 以前,如需测试工作器,您需要使用 WorkManagerTestInitHelper 来初始化 WorkManager。从 2.1.0 开始,如果要测试 Worker 的实现,将无需使用 WorkManagerTestInitHelper

12.1测试工作器

假设我们有一个类似以下示例的 Worker

class SleepWorker(context: Context, parameters: WorkerParameters) :
    Worker(context, parameters) {

    override fun doWork(): Result {
        // Sleep on a background thread.
        Thread.sleep(1000)
        return Result.success()
    }
}

如需测试这个 Worker,您可以使用 TestWorkerBuilder。此构建器有助于构建可用于测试业务逻辑的 Worker 实例。

// Kotlin code uses 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
        ).build()

        val result = worker.doWork()
        assertThat(result, `is`(Result.success()))
    }
}
//测试类,这些哪个模块用就要放到哪一个模块里,不能testApi,androidTestApi这样引用。
testImplementation "junit:junit:${junitVersion}"
androidTestImplementation "androidx.test.ext:junit:${androidXTestExtJunitVersion}"
androidTestImplementation "androidx.test.espresso:espresso-core:${androidXTestEspressoCoreVersion}"
testImplementation "androidx.test.ext:junit-ktx:${androidXTestExtJunitVersion}"

12.2 传递inputDatarunAttemptCount

TestWorkerBuilder 也可用于设置标记(例如 inputDatarunAttemptCount),以便您单独验证工作器状态。例如,SleepWorker 将休眠时长当作输入数据,而不是工作器中定义的常量数据:

class SleepWorker(context: Context, parameters: WorkerParameters) :
    Worker(context, parameters) {

    override fun doWork(): Result {
        // Sleep on a background thread.
        val sleepDuration = inputData.getLong(SLEEP_DURATION, 1000)
        Thread.sleep(sleepDuration)
        return Result.success()
    }

    companion object {
        const val SLEEP_DURATION = "SLEEP_DURATION"
    }
}

SleepWorkerTest 中,您可以将该输入数据提供给 TestWorkerBuilder,以满足 SleepWorker 的需求。

// Kotlin code uses 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 1000L)
        ).build()

        val result = worker.doWork()
        assertThat(result, `is`(Result.success()))
    }
}

如需详细了解 TestWorkerBuilder API,请参阅 TestListenableWorkerBuilderTestWorkerBuilder 的父类)的参考页面。

12.3 测试 ListenableWorker 及其变体

如需测试 ListenableWorker 或其变体(CoroutineWorkerRxWorker),请使用 TestListenableWorkerBuilderTestWorkerBuilderTestListenableWorkerBuilder 的主要区别在于,TestWorkerBuilder 允许指定用来运行 Worker 的后台 Executor,而 TestListenableWorkerBuilder 依赖 ListenableWorker 实现的线程逻辑。

例如,假设我们需要测试类似以下示例的 CoroutineWorker

class SleepWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {
    override suspend fun doWork(): Result {
        delay(1000L) // milliseconds
        return Result.success()
    }
}

    WorkManager

    方法指南
    
    - [定义您的 WorkRequest](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/define-work)
    - [工作状态](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/states-and-observation)
    - [管理工作](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/managing-work)
    - [观察工作器的中间进度](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/intermediate-progress)
    - [将工作链接在一起](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/chain-work)
    - [测试 Worker 实现](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/testing-worker-impl)
    - [使用 WorkManager 进行集成测试](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/integration-testing)
    - [调试 WorkManager](https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/debugging)
    

    -

    高级概念
    

导航组件

Intent 和 Intent 过滤器

界面

动画和过渡

图片和图形

音频和视频

服务

后台任务

权限

应用数据和文件

用户数据和身份

用户位置

触摸和输入

CameraX

Camera2

相机

传感器

连接性

Renderscript

基于网络的内容

Android App Bundle

Google Play

切片

  • 游戏

开发

优化

发布和迭代

  • 最佳做法

依赖项注入

测试

性能

无障碍

隐私设置

安全性

为数十亿用户打造产品

为企业打造产品

本页内容测试工作器测试 ListenableWorker 及其变体

文档

指南

13.测试 Worker 实现

WorkManager 提供了用于测试 WorkerListenableWorkerListenableWorker 变体(CoroutineWorkerRxWorker)的 API。

注意:在版本 2.1.0 以前,如需测试工作器,您需要使用 WorkManagerTestInitHelper 来初始化 WorkManager。从 2.1.0 开始,如果要测试 Worker 的实现,将无需使用 WorkManagerTestInitHelper

13.1 测试工作器

假设我们有一个类似以下示例的 Worker

KotlinJava

class SleepWorker(context: Context, parameters: WorkerParameters) :
    Worker(context, parameters) {

    override fun doWork(): Result {
        // Sleep on a background thread.
        Thread.sleep(1000)
        return Result.success()
    }
}

如需测试这个 Worker,您可以使用 TestWorkerBuilder。此构建器有助于构建可用于测试业务逻辑的 Worker 实例。

KotlinJava

// Kotlin code uses 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
        ).build()

        val result = worker.doWork()
        assertThat(result, `is`(Result.success()))
    }
}

TestWorkerBuilder 也可用于设置标记(例如 inputDatarunAttemptCount),以便您单独验证工作器状态。例如,SleepWorker 将休眠时长当作输入数据,而不是工作器中定义的常量数据:

KotlinJava

class SleepWorker(context: Context, parameters: WorkerParameters) :
    Worker(context, parameters) {

    override fun doWork(): Result {
        // Sleep on a background thread.
        val sleepDuration = inputData.getLong(SLEEP_DURATION, 1000)
        Thread.sleep(sleepDuration)
        return Result.success()
    }

    companion object {
        const val SLEEP_DURATION = "SLEEP_DURATION"
    }
}

SleepWorkerTest 中,您可以将该输入数据提供给 TestWorkerBuilder,以满足 SleepWorker 的需求。

KotlinJava

// Kotlin code uses 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 1000L)
        ).build()

        val result = worker.doWork()
        assertThat(result, `is`(Result.success()))
    }
}

如需详细了解 TestWorkerBuilder API,请参阅 TestListenableWorkerBuilderTestWorkerBuilder 的父类)的参考页面。

13.2 测试 ListenableWorker 及其变体

如需测试 ListenableWorker 或其变体(CoroutineWorkerRxWorker),请使用 TestListenableWorkerBuilderTestWorkerBuilderTestListenableWorkerBuilder 的主要区别在于,TestWorkerBuilder 允许指定用来运行 Worker 的后台 Executor,而 TestListenableWorkerBuilder 依赖 ListenableWorker 实现的线程逻辑。

例如,假设我们需要测试类似以下示例的 CoroutineWorker

class SleepWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {
    override suspend fun doWork(): Result {
        delay(1000L) // milliseconds
        return Result.success()
    }
}

为了测试 SleepWorker,我们首先使用 TestListenableWorkerBuilder 创建一个工作器实例,然后在协程中调用其 doWork 函数。

@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
    private lateinit var context: Context

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
    }

    @Test
    fun testSleepWorker() {
        val worker = TestListenableWorkerBuilder<SleepWorker>(context).build()
        runBlocking {
            val result = worker.doWork()
            assertThat(result, `is`(Result.success()))
        }
    }
}

runBlocking 可用作测试的协程构建器,使任何异步执行的代码都转为并行运行。

测试 RxWorker 实现类似于测试 CoroutineWorker,因为 TestListenableWorkerBuilder 可以处理 ListenableWorker 的任何子类。假设某个版本的 SleepWorker 使用 RxJava 而非协程。

class SleepWorker(
    context: Context,
    parameters: WorkerParameters
) : RxWorker(context, parameters) {
    override fun createWork(): Single<Result> {
        return Single.just(Result.success())
            .delay(1000L, TimeUnit.MILLISECONDS)
    }
}

测试 RxWorkerSleepWorkerTest 版本可能类似于测试 CoroutineWorker 的版本。您使用相同的 TestListenableWorkerBuilder,但现在会调用 RxWorkercreateWork 函数。createWork 会返回一个 Single,可用于验证工作器的行为。TestListenableWorkerBuilder 可处理线程方面的任何复杂问题,还能并行执行工作器代码。

@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
    private lateinit var context: Context

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
    }

    @Test
    fun testSleepWorker() {
        val worker = TestListenableWorkerBuilder<SleepWorker>(context).build()
        worker.createWork().subscribe { result ->
            assertThat(result, `is`(Result.success()))
        }
    }
}

14.使用 WorkManager 进行集成测试

详情请查看原文

15.调试 WorkManager

如果您发现您的工作器运行过于频繁或根本没有运行,请按照以下几个调试步骤操作,以发现可能存在的问题。

15.1启用日志记录

如需确定工作器未正确运行的原因,查看详细的 WorkManager 日志很有帮助。如需启用日志记录功能,您需要使用自定义初始化。首先,通过创建应用了清单合并规则 remove 的新 WorkManager 提供程序来停用 AndroidManifest.xml 中的默认 WorkManagerInitializer

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    tools:node="remove"/>

现在,默认 WorkManager 初始化程序已停用,您可以使用按需初始化。为此,android.app.Application 类必须提供 androidx.work.Configuration.Provider 的实现。

class MyApplication() : Application(), Configuration.Provider {
    override fun getWorkManagerConfiguration() =
        Configuration.Builder()
            .setMinimumLoggingLevel(android.util.Log.DEBUG)
            .build()
}

在您定义自定义 WorkManager 配置后,WorkManager 会在您调用 WorkManager.getInstance(Context) 时进行初始化,而不是在应用启动时自动初始化。如需了解详情,包括对 2.1.0 之前的 WorkManager 版本提供的支持,请参阅自定义 WorkManager 配置和初始化

启用调试日志记录后,系统会开始显示更多包含日志标记前缀 WM- 的日志。

15.2 使用 adb shell dumpsys jobscheduler

搭载 API 级别 23 或更高版本时,您可以运行命令 adb shell dumpsys jobscheduler 来查看已归因于您的软件包的作业列表。

您应该会看到与以下类似的内容:

详情请查看原文

16.自定义 WorkManager 配置和初始化

​ 默认情况下,当您的应用启动时,WorkManager 使用适合大多数应用的合理选项自动进行配置。如果您需要进一步控制 WorkManager 管理和调度工作的方式,可以通过自行初始化 WorkManager 来自定义 WorkManager 配置。

为 WorkManager 提供自定义初始化的最灵活方式是使用 WorkManager 2.1.0 及更高版本中提供的按需初始化。其他选项稍后讨论

16.1 按需初始化

通过按需初始化,您可以仅在需要 WorkManager 时创建该组件,而不必每次应用启动时都创建。这样做可将 WorkManager 从关键启动路径中移出,从而提高应用启动性能。如需使用按需初始化,请执行以下操作:

16.1.1 移除默认初始化程序

如需提供自己的配置,必须先移除默认初始化程序。为此,请使用合并规则 tools:node="remove" 更新 AndroidManifest.xml

从 WorkManager 2.6 开始,应用启动功能便已在 WorkManager 内部使用。如需提供自定义初始化程序,您需要移除 androidx.startup 节点。

如果您不在应用中使用应用启动功能,则可以将其彻底移除。

 <!-- If you want to disable android.startup completely. -->
 <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove">
 </provider>

否则,仅移除 WorkManagerInitializer 节点即可。

 <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <!-- If you are using androidx.startup to initialize other components -->
    <meta-data
        android:name="androidx.work.WorkManagerInitializer"
        android:value="androidx.startup"
        tools:node="remove" />
 </provider>

如果您使用的 WorkManager 是 2.6 之前的版本,请改为移除 workmanager-init

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    tools:node="remove" />

如需详细了解如何在清单中使用合并规则,请参阅有关合并多个清单文件的文档。

16.1.2实现 Configuration.Provider

让您的 Application 类实现 Configuration.Provider 接口,并提供您自己的 Configuration.Provider.getWorkManagerConfiguration() 实现。当您需要使用 WorkManager 时,请务必调用方法 WorkManager.getInstance(Context)。WorkManager 会调用应用的自定义 getWorkManagerConfiguration() 方法来发现其 Configuration。(您无需自行调用 WorkManager.initialize()。)

注意:如果您在初始化 WorkManager 之前调用已弃用的无参数 WorkManager.getInstance() 方法,该方法将抛出异常。即使您不自定义 WorkManager,您也应始终使用 WorkManager.getInstance(Context) 方法。

以下示例展示了自定义 getWorkManagerConfiguration() 实现:

class MyApplication() : Application(), Configuration.Provider {
     override fun getWorkManagerConfiguration() =
           Configuration.Builder()
                .setMinimumLoggingLevel(android.util.Log.INFO)
                .build()
}

注意: 您必须移除默认的初始化程序,自定义的 getWorkManagerConfiguration() 实现才能生效。

16.2 WorkManager 2.1.0 之前版本的自定义初始化

对于 WorkManager 2.1.0 之前的版本,有两个初始化选项。在大多数情况下,默认初始化就足够了。如需更精确地控制 WorkManager,可以指定您自己的配置

16.2.1默认初始化

在您的应用启动时,WorkManager 会使用自定义 ContentProvider 自行初始化。此代码位于内部类 androidx.work.impl.WorkManagerInitializer 中,并使用默认 Configuration。自动使用默认初始化程序(除非明确停用它)。默认初始化程序适合大多数应用。

16.2.2 自定义初始化

如果您想控制初始化过程,就必须停用默认初始化程序,然后定义您自己的自定义配置。

移除默认初始化程序后,可以手动初始化 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() 参考文档。

17.高级概念

17.1 自定义 WorkManager 配置和初始化

详情请查看原文

17.2 WorkManager 中的线程处理

详情请查看原文

18. 支持长时间运行的工作器

WorkManager 2.3.0-alpha02 增加了对长时间运行的工作器的内置支持。 在这种情况下,WorkManager 可以向操作系统提供一个信号,指示在此项工作执行期间应尽可能让进程保持活跃状态。这些工作器可以运行超过 10 分钟。这一新功能的示例用例包括批量上传或下载(不可分块)、在本地进行的机器学习模型处理,或者对应用的用户很重要的任务。

在后台,WorkManager 会代表您管理和运行前台服务以执行 WorkRequest,同时还会显示可配置的通知。

ListenableWorker 现在支持 setForegroundAsync() API,而 CoroutineWorker 则支持挂起 setForeground() API。这些 API 允许开发者指定此 WorkRequest 是“重要的”(从用户的角度来看)或“长时间运行的”任务。

2.3.0-alpha03 开始,WorkManager 还允许您创建 PendingIntent,此 Intent 可用于取消工作器,而不必使用 createCancelPendingIntent() API 注册新的 Android 组件。此方法与 setForegroundAsync()setForeground() API 一起使用时特别有用,可用于添加一个取消 Worker 的通知操作。

详情请查看