WorkManager
WorkManager 是Android最新一代的后台调度 API,主要是用来替换先前的旧版后台调度 API(如 JobScheduler 等)。
WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。
持久性工作的类型
WorkManager 可处理三种类型的持久性工作:
- 立即执行:必须立即开始且很快就完成的任务,可以加急。
- 长时间运行:运行时间可能较长(有可能超过 10 分钟)的任务。
- 可延期执行:延期开始并且可以定期运行的预定任务。
图 1 大致表明了不同类型的持久性工作彼此之间的关系。
下表大致列出了各种工作类型:
类型 | 周期 | 使用方式 |
---|---|---|
立即 | 一次性 | OneTimeWorkRequest 和 Worker。如需处理加急工作,请对 OneTimeWorkRequest 调用 setExpedited()。 |
长期运行 | 一次性或定期 | 任意 WorkRequest 或 Worker。在工作器中调用 setForeground() 来处理通知。 |
可延期 | 一次性或定期 | PeriodicWorkRequest 和 Worker。 |
使用 WorkManager 的优势
除了具备更为简单且一致的 API 之外,WorkManager 还具备许多其他关键优势:
-
工作约束: 使用工作约束明确定义工作运行的最佳条件。例如,仅在设备采用不按流量计费的网络连接时、当设备处于空闲状态或者有足够的电量时运行。
-
强大的调度: WorkManager 允许您使用灵活的调度窗口调度工作,以运行一次性或重复工作。您还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。
已调度的工作存储在内部托管的 SQLite 数据库中,由 WorkManager 负责确保该工作持续进行,并在设备重新启动后重新调度。
此外,WorkManager 遵循低电耗模式等省电功能和最佳做法,因此您在这方面无需担心。 -
加急工作: 您可以使用 WorkManager 调度需在后台立即执行的工作。您应该使用加急工作来处理对用户来说很重要且会在几分钟内完成的任务。
-
灵活的重试政策: 有时工作会失败,WorkManager 提供了灵活的重试政策,包括可配置的指数退避政策。
-
工作链: 对于复杂的相关工作,您可以使用直观的接口将各个工作任务串联起来,这样您便可以控制哪些部分依序运行,哪些部分并行运行。对于每项工作任务,您可以定义工作的输入和输出数据。将工作串联在一起时,WorkManager 会自动将输出数据从一个工作任务传递给下一个工作任务。
WorkManager.getInstance(myContext)
.beginWith(workA)
.then(workB)
.then(listOf(workC1, workC2, workC3)
.then(workC)
.then(if (save) workD else workE)
.enqueue()
- 内置线程互操作性: WorkManager 无缝集成 Coroutines 和 RxJava,让您可以插入自己的异步 API,非常灵活。
注意:尽管 Coroutines 和 WorkManager 分别是针对于不同用例推荐的解决方案,但它们二者并不是互斥的关系。您可以在通过 WorkManager 调度的工作中使用协程。
使用 WorkManager 保证工作可靠性
WorkManager 适用于需要可靠运行的工作,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。例如:
- 向后端服务发送日志或分析数据。
- 定期将应用数据与服务器同步。
一个最好用的场景是:当网络状态恢复时执行某个后台任务。可靠性:指定条件的任务,哪怕App进程被杀了,也一定会执行。
WorkManager 不适用于那些可在应用进程结束时安全终止的进程内后台工作。它也并非对所有需要立即执行的工作都适用的通用解决方案。
与其他 API 的关系
虽然协程是适合某些用例的推荐解决方案,但您不应将其用于持久性工作。请务必注意,协程是一个并发框架,而 WorkManager 是一个持久性工作库。同样,AlarmManager 仅适合用于时钟或日历。
API | 推荐使用场景 | 与 WorkManager 的关系 |
---|---|---|
Coroutines | 所有不需要持久的异步工作。 | 协程是在 Kotlin 中退出主线程的标准方式。不过,它们在应用关闭后会释放内存。对于持久性工作,请使用 WorkManager。 |
AlarmManager | 仅限闹钟。 | 与 WorkManager 不同,AlarmManager 会使设备从低电耗模式中唤醒。因此,它在电源和资源管理方面来讲并不高效。AlarmManager 仅适合用于精确闹钟或通知(例如日历活动)场景,而不适用于后台工作。 |
WorkManager 的使用
添加依赖
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")
}
一般只需使用work-runtime-ktx
版本的就可以了。
WorkManager 的使用主要包括三个核心类:
定义 Worker
工作使用 Worker
类定义。 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()
:工作失败,应根据其重试政策在其他时间尝试。
创建 WorkRequest
定义工作后,必须使用 WorkManager
服务进行调度该工作才能运行。对于如何调度工作,WorkManager
提供了很大的灵活性。您可以将其安排为在某段时间内定期运行,也可以将其安排为仅运行一次。
WorkRequest
对象包含 WorkManager
调度和运行工作所需的所有信息。其中包括运行工作必须满足的约束、调度信息(例如延迟或重复间隔)、重试配置,并且可能包含输入数据(如果工作需要)。Worker
负责定义工作内容,WorkRequest
则负责定义工作运行方式和时间。
执行工作器的确切时间取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供最佳行为。
WorkRequest
本身是抽象基类。该类有两个派生实现:
OneTimeWorkRequest
:适用于调度非重复性的一次性工作PeriodicWorkRequest
适合调度以一定间隔重复执行的工作
调度一次性工作
对于无需额外配置的简单工作,请使用静态方法 from:
val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)
对于更复杂的工作,可以使用构建器:
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<MyWork>()
// Additional configuration
.build()
最后,您需要使用 enqueue()
方法将 WorkRequest
提交到 WorkManager
WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)
调度加急工作
WorkManager 2.7.0
引入了加急工作的概念。这使 WorkManager 能够执行重要工作,同时使系统能够更好地控制对资源的访问权限。
加急工作具有以下特征:
- 重要性:加急工作适用于对用户很重要或由用户启动的任务。
- 速度:加急工作最适合那些立即启动并在几分钟内完成的简短任务。
- 配额:限制前台执行时间的系统级配额决定了加急作业是否可以启动。
- 电源管理:电源管理限制(如省电模式和低电耗模式)不太可能影响加急工作。
- 延迟时间:系统立即执行加急工作,前提是系统的当前工作负载允许执行此操作。这意味着这些工作对延迟时间较为敏感,不能安排到以后执行。
在用户想要发送消息或附加的图片时,可能会在聊天应用内使用加急工作。同样,处理付款或订阅流程的应用也可能需要使用加急工作。这是因为这些任务对用户很重要,会在后台快速执行,并需要立即开始执行。
可以通过调用 setExpedited()
来声明 WorkRequest
应该使用加急作业,以尽可能快的速度运行。例如以下代码示例:
val request = OneTimeWorkRequestBuilder()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context).enqueue(request)
在此示例中,我们初始化 OneTimeWorkRequest
的实例并对其调用 setExpedited()
,然后,此请求就会变成加急工作。如果配额允许,它将立即开始在后台运行。
为了保持加急作业的向后兼容性,WorkManager
可能会在 Android 12
之前版本的平台上运行前台服务。前台服务可以向用户显示通知。
在 Android 12
之前,工作器中的 getForegroundInfoAsync()
和 getForegroundInfo()
方法可让 WorkManager
在您调用 setExpedited()
时显示通知。
如果您想要请求任务作为加急作业运行,则所有的 ListenableWorker
都必须实现 getForegroundInfo
方法。
注意:如果未能实现对应的
getForegroundInfo
方法,那么在旧版平台上调用setExpedited
时,可能会导致运行时崩溃。
工作器不知道自身所执行的工作是否已加急。不过,在某些版本的 Android 上,如果 WorkRequest 被加急,工作器可以显示通知。
为此,WorkManager 提供了 getForegroundInfoAsync()
方法,您必须实现该方法,让 WorkManager 在必要时显示通知,以便启动 ForegroundService
。
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 {
try {
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
return Result.failure()
}
// TODO()
}
private fun createNotification() : Notification {
// TODO()
}
}
注意:您应该将
setForeground()
封装在try/catch
块中,以捕获可能出现的IllegalStateException
。如果您的应用此时无法在前台运行,便可能会发生这类异常。在Android 12
及更高版本中,您可以使用更详细的ForegroundServiceStartNotAllowedException
。
配额策略
系统必须先为加急作业分配应用执行时间,然后才能运行作业。执行时间并非无限制,而是受配额限制。如果您的应用使用其执行时间并达到分配的配额,在配额刷新之前,您无法再执行加急工作。这样,Android 可以更有效地在应用之间平衡资源。
每个应用均有自己的前台执行时间配额。可用的执行时间取决于待机模式存储分区和进程的重要性。
注意:当您的应用在前台运行时,配额不会限制加急工作的执行。仅当应用在后台运行或移至后台时,执行时间配额才适用。因此,您应加急想在后台继续的工作。当应用在前台运行时,您可以继续使用 setForeground()。
您可以控制当应用达到其执行配额时加急工作需要做出如何反应,可以通过 setExpedited()
设置如下值:
OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
,这会导致作业作为普通工作继续请求运行。OutOfQuotaPolicy.DROP_WORK_REQUEST
,这会在配额不足时导致请求取消。
延迟加急工作
系统会尝试在调用指定的加急作业后,尽快执行该作业。不过,与其他类型的作业一样,如果出现以下情况下,系统可能会延迟启动新的加急工作:
- 系统负载过高:当有过多作业已在运行或者当系统内存不足时,就会发生这种情况。
- 已超出加急作业配额限制:加急工作使用基于应用待机存储分区的配额系统,并限制滚动时间窗口中的最大执行时间。用于加急工作的配额比用于其他类型的后台作业的配额限制性更强。
调度定期工作
您的应用有时可能需要定期运行某些工作。例如,您可能要定期备份数据、定期下载应用中的新鲜内容或者定期上传日志到服务器。
使用 PeriodicWorkRequest
创建定期执行的 WorkRequest
对象的方法如下:
val saveRequest =
PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
// Additional configuration
.build()
在此示例中,工作的运行时间间隔定为一小时。
时间间隔定义为两次重复执行之间的最短时间。工作器的确切执行时间取决于您在 WorkRequest 对象中设置的约束以及系统执行的优化。(注意:可以定义的最短重复间隔是 15 分钟
)
注意:WorkManager调度定期工作最少15分钟执行一次重复任务,它只能保证任务一定执行,但不能保证指定的精确时间执行。
弹性运行间隔
如果您的工作的性质致使其对运行时间敏感,您可以将 PeriodicWorkRequest
配置为在每个时间间隔的弹性时间段内运行,如图 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分钟)。
设置 Worker 的约束
您可以对定期工作设置约束。例如,您可以为工作请求添加约束,以便工作仅在用户设备充电时运行。在这种情况下,除非满足约束条件,否则即使过了定义的重复间隔,PeriodicWorkRequest 也不会运行。这可能会导致工作在某次运行时出现延迟,甚至会因在相应间隔内未满足条件而被跳过。
约束可确保将工作延迟到满足最佳条件时运行。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 将停止工作器。系统将在满足所有约束后重试工作。
调度延期工作
如果工作没有约束,或者当工作加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动。
下面举例说明了如何将工作设置为在加入队列后至少经过 10 分钟后再运行。
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setInitialDelay(10, TimeUnit.MINUTES)
.build()
该示例说明了如何为 OneTimeWorkRequest
设置初始延迟时间,您也可以为 PeriodicWorkRequest
设置初始延迟时间(在这种情况下,定期工作只有首次运行时会延迟。)。
注意:执行工作器的确切时间还取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供可能的最佳行为。
重试和退避策略
如果您需要让 WorkManager
重试工作,可以在 doWork()
方法返回 Result.retry()
。然后,系统将根据退避延迟时间和退避策略重新调度工作。
-
退避延迟时间 指定了首次尝试后重试工作前的最短等待时间。此值不能超过
10 秒
(或MIN_BACKOFF_MILLIS
)。 -
**退避策略 ** 定义了在后续重试过程中,退避延迟时间随时间以怎样的方式增长。WorkManager 支持
2
个退避策略,即LINEAR
和EXPONENTIAL
。
每个工作请求都有退避策略和退避延迟时间。默认策略是 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 秒
,以此类推。
注意:退避延迟时间不精确,在两次重试之间可能会有几秒钟的差异,但绝不会低于配置中指定的初始退避延迟时间。
标记 Worker
每个工作请求都有一个唯一标识符,该标识符可用于在以后标识该工作,以便取消工作或观察其进度。
如果有一组在逻辑上相关的工作,对这些工作项进行标记可能也会很有帮助。通过标记,您可以将一组工作请求一起处理。例如,WorkManager.cancelAllWorkByTag(String)
会取消带有特定标记的所有工作请求,WorkManager.getWorkInfosByTag(String)
会返回一个 WorkInfo
对象列表,该列表可用于确定当前工作状态。
要标记一个 Worker
,请使用 WorkRequest 构建器的 addTag()
方法。以下代码展示了如何向工作添加“cleanup
”标记:
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.addTag("cleanup")
.build()
最后,可以向单个工作请求添加多个标记。这些标记在内部以一组字符串的形式进行存储。您可以使用 WorkInfo.getTags()
获取与 WorkRequest
关联的标记集。在 Worker
类中,您可以通过 getTags()
方法检索其标记集。
向 Worker 传递参数
通过 WorkRequest
构建器创建 Worker
对象时,可以调用 setInputData()
方法设置需要向 Worker
对象传递的参数内容。
参数内容是以键值对Pair
的形式存储在 Data
对象中, WorkManager
会在执行工作时将输入 Data
传递给Worker
对象。在 Worker
类中可通过调用 Worker.getInputData()
来获取输入参数。
例如,处理图片上传的工作可能需要使用待上传图片的 URI 作为输入数据:
class UploadWork(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
val imageUriInput = inputData.getString("IMAGE_URI") ?: return Result.failure() // 根据key来查询
uploadFile(imageUriInput)
return Result.success()
}
...
}
val myUploadWork = OneTimeWorkRequestBuilder<UploadWork>()
.setInputData(workDataOf("IMAGE_URI" to "http://xxx")) // 支持多个 key to value 的 Pair 对
.build()
同样,可使用 Data
类输出返回值,例如返回 Result.success(Data)
。任务的 WorkInfo 中会提供返回值:
WorkManager.getInstance(myContext).getWorkInfoByIdLiveData(mathWork.id)
.observe(this, Observer { info ->
if (info != null && info.state.isFinished) {
val myResult = info.outputData.getInt(KEY_RESULT, myDefaultValue)
// ... do something with the result ...
}
})
如果您链接多个任务,一个任务的输出可以作为任务链中下一个任务的输入。如果是简单链(即一个 OneTimeWorkRequest
后面跟着另一个 OneTimeWorkRequest
),第一个任务通过调用 Result.success(Data)
返回其结果,下一个任务通过调用 getInputData()
提取该结果。如果是更复杂的链(例如有多个任务都将输出发送给同一个后续任务),您可以在 OneTimeWorkRequest.Builder
上定义 InputMerger
,以指定当多个不同任务返回具有相同键的输出时应执行什么操作。
Worker 的状态
一次性工作的状态
对于 one-time
工作请求,工作的初始状态为 ENQUEUED
。
在 ENQUEUED
状态下,您的工作会在满足其约束和初始延迟计时要求后立即运行。接下来,该工作会转为 RUNNING
状态,然后可能会根据工作的结果转为 SUCCEEDED
、FAILED
状态;或者,如果结果是 retry
,它可能会回到 ENQUEUED
状态。在此过程中,随时都可以取消工作,取消后工作将进入 CANCELLED
状态。
SUCCEEDED
、FAILED
和 CANCELLED
均表示此工作的终止状态。如果您的工作处于上述任何状态,WorkInfo.State.isFinished()
都将返回 true
。
定期工作的状态
成功和失败状态仅适用于一次性工作和链式工作。定期工作只有一个终止状态 CANCELLED
。这是因为定期工作永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。
管理 Worker
定义 Worker
和 WorkRequest
后,最后一步是将工作加入队列。将工作加入队列的最简单方法是调用 WorkManager enqueue()
方法,然后传递要运行的 WorkRequest
。
val myWork: WorkRequest = // ... OneTime or PeriodicWork
WorkManager.getInstance(context).enqueue(myWork)
在将工作加入队列时请小心谨慎,以避免重复。例如,应用可能会每 24 小时尝试将其日志上传到后端服务。如果不谨慎,即使作业只需运行一次,您最终也可能会多次将同一作业加入队列。为了实现此目标,您可以将工作调度为唯一工作。
Unique Worker
Unique Worker 是一个很实用的概念,可确保同一时刻只有一个具有特定名称的工作实例。与 ID
不同的是,Unique Worker 的名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,Unique Worker 的名称仅与一个工作实例相关联。
Unique Worker既可用于一次性工作,也可用于定期工作。您可以通过调用以下方法之一创建Unique Worker序列,具体取决于您是调度重复工作还是一次性工作:
WorkManager.enqueueUniqueWork()
(用于一次性工作)WorkManager.enqueueUniquePeriodicWork()
(用于定期工作)
这两种方法都接受 3 个参数:
uniqueWorkName
- 用于唯一标识工作请求的String
类型名称。existingWorkPolicy
- 此enum
可告知WorkManager
:如果已有使用该名称且尚未完成的唯一工作链时,应执行什么策略(见下文)。work
- 要调度的WorkRequest
。
借助Unique Worker,我们可以解决前面提到的重复调度问题。
val sendLogsWorkRequest =
PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)
.setConstraints(Constraints.Builder().setRequiresCharging(true).build())
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"sendLogs",
ExistingPeriodicWorkPolicy.KEEP,
sendLogsWorkRequest)
现在,如果上述代码在 sendLogs 作业已处于队列中的情况下运行,系统会保留现有的作业,并且不会添加新的作业。
当您需要逐步构建一个长任务链时,也可以利用唯一工作序列。例如,照片编辑应用可能允许用户撤消一长串操作。其中的每一项撤消操作可能都需要一些时间来完成,但必须按正确的顺序执行。在这种情况下,应用可以创建一个“撤消”链,并根据需要将每个撤消操作附加到该链上。
冲突解决策略
调度 Unique Worker 时,必须告知 WorkManager 在发生冲突时要执行的操作。您可以通过在将工作加入队列时传递一个枚举值来实现此目的。
对于一次性工作,可以设置 ExistingWorkPolicy
,它提供了用于处理冲突的 4 个选项:
-
REPLACE:用新工作替换现有工作。此选项将取消现有工作。
-
KEEP:保留现有工作,并忽略新工作。
-
APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。
现有工作将成为新工作的先决条件。如果现有工作变为CANCELLED
或FAILED
状态,新工作也会变为CANCELLED
或FAILED
。如果您希望无论现有工作的状态如何都运行新工作,请改用APPEND_OR_REPLACE
。 -
APPEND_OR_REPLACE 函数类似于
APPEND
,不过它并不依赖于先决条件工作状态。即使现有工作变为CANCELLED
或FAILED
状态,新工作仍会运行。
对于定期工作,您需要提供一个 ExistingPeriodicWorkPolicy
,它支持 REPLACE 和 KEEP 这两个选项。选项的功能与其对应的 ExistingWorkPolicy
功能相同。
观察/查询 Worker 的信息
在将Worker
加入队列后,您可以随时按其 name
、id
或与其关联的 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
, WorkInfo
对象中包含了 Worker 的 id
、tag
标记、当前的状态 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.getWorkInfoByIdLiveData()
返回的是一个LiveData
对象,可以给它添加一个Observer
观察者回调。
需要注意的是,由于LiveData
的特性,当doWork
执行完后,发送给Activity中注册的观察者回调时,会收到多次,因为每一种状态都会执行回调,只有当 isFinished=true
时,才表示任务真的执行完成了。(LiveData绑定Activity的生命周期的观察者回调,在onCreate、onResume都会回调)如果不使用LiveData,就只会有一次。
WorkManager.getInstance(myContext).getWorkInfoByIdLiveData(mathWork.id).observe(this, Observer { info ->
if (info != null && info.state.isFinished) {
val myResult = info.outputData.getInt(KEY_RESULT, myDefaultValue)
// ... do something with the result ...
}
})
使用 WorkQuery 进行复杂的 Worker 查询
WorkManager 2.4.0
及更高版本支持使用 WorkQuery
对象对已加入队列的作业进行复杂查询。WorkQuery
支持按工作的标记、状态和 Unique Worker
名称的组合进行查询。
以下示例说明了如何查找带有“syncTag
”标记、处于 FAILED
或 CANCELLED
状态、且 Unique Worker
名称为“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
逻辑关系。
例如:(name1 OR name2 OR ... ) AND (tag1 OR tag2 OR ...) AND (state1 OR state2 OR ...)
。
WorkQuery
也适用于等效的 LiveData
方法 getWorkInfosLiveData()
。
取消和停止工作
如果您不再需要运行先前加入队列的工作, 可以按工作的 name
、id
或与其关联的 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)
会取消具有给定标记的所有工作。
监听 Worker 的停止
正在运行的 Worker 可能会由于以下几种原因而停止运行:
- 您明确要求取消它(例如,通过调用
WorkManager.cancelWorkById(UUID)
取消)。 - 如果是唯一工作,您明确地将
ExistingWorkPolicy
为REPLACE
的新WorkRequest
加入到了队列中。旧的WorkRequest
会立即被视为已取消。 - 您的工作约束条件已不再满足。
- 系统出于某种原因指示您的应用停止工作。如果超过 10 分钟的执行期限,可能会发生这种情况。该工作会调度为在稍后重试。
在这些情况下,您的工作器会停止。
您应该合作地取消正在进行的任何工作,并释放您的工作器保留的所有资源。例如,此时应该关闭所打开的数据库和文件句柄。有两种机制可让您获取工作器何时停止:
-
onStopped() 回调: 在您的工作器停止后,
WorkManager
会立即调用ListenableWorker.onStopped()
。覆写此方法可关闭您可能保留的所有资源。 -
isStopped() 属性: 您可以调用
ListenableWorker.isStopped()
方法以检查工作器是否已停止。如果您在工作器中执行长时间运行的操作或重复操作,您应经常检查此属性,并将其用作尽快停止工作的信号。
注意:
WorkManager
会忽略已收到onStop
信号的工作器所设置的Result
,因为工作器已被视为停止。
观察 Worker 的中间进度
如果应用在前台运行时,工作器保持运行状态,那么可以使用返回 WorkInfo 的 LiveData 的 API 向用户显示此信息。
ListenableWorker
现在支持 setProgressAsync()
API,此类 API 允许保留中间进度。借助这些 API,开发者能够设置可通过界面观察到的中间进度。进度由 Data
类型表示,这是一个可序列化的属性容器(类似于 input
和 output
,并且受到相同的限制)。
只有在 ListenableWorker
运行时才能观察到和更新进度信息。如果尝试在 ListenableWorker
完成执行后在其中设置进度,则将会被忽略。您还可以使用 getWorkInfoByXXX()
或 getWorkInfoByXXXLiveData()
方法来观察进度信息。这两个方法会返回 WorkInfo
的实例,后者有一个返回 Data
类型的新 getProgress()
方法。
更新进度
对于使用 ListenableWorker
或 Worker
的 Java
开发者,setProgressAsync()
API 会返回 ListenableFuture<Void>
;更新进度是异步过程,因为更新过程涉及将进度信息存储在数据库中。在 Kotlin
中,您可以使用 CoroutineWorker
对象的 setProgress()
扩展函数来更新进度信息。
此示例展示了一个简单的 ProgressWorker
。Worker
在启动时将进度设置为 0
,在完成后将进度值更新为 100
。
// Kotlin 版本
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()
}
}
// Java 版本
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class ProgressWorker extends Worker {
private static final String PROGRESS = "PROGRESS";
private static final long DELAY = 1000L;
public ProgressWorker(
@NonNull Context context,
@NonNull WorkerParameters parameters) {
super(context, parameters);
// Set initial progress to 0
setProgressAsync(new Data.Builder().putInt(PROGRESS, 0).build());
}
@NonNull
@Override
public Result doWork() {
try {
// Doing work.
Thread.sleep(DELAY);
} catch (InterruptedException exception) {
// ... handle exception
}
// Set progress to 100 after you are done doing your work.
setProgressAsync(new Data.Builder().putInt(PROGRESS, 100).build());
return Result.success();
}
}
观察进度
观察进度信息也很简单,可以使用 getWorkInfoByXXX()
或 getWorkInfoByXXXLiveData()
方法,并引用 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
}
})
链接 Worker
您可以使用 WorkManager
创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序。当您需要以特定顺序运行多个任务时,此功能尤其有用。
例如,假设您的应用有三个 OneTimeWorkRequest
对象:workA
、workB
和 workC
。这些任务必须按该顺序运行。如需对这些任务进行排队,请使用 WorkManager.beginWith(OneTimeWorkRequest)
方法,该方法会返回一个 WorkContinuation
实例。
然后,可以使用 WorkContinuation
的 then(OneTimeWorkRequest)
或 then(List<OneTimeWorkRequest>)
添加剩余的 OneTimeWorkRequest
依赖实例。每次调用 WorkContinuation.then(...)
都会返回一个新的 WorkContinuation
实例。如果添加了一个 List<OneTimeWorkRequest>
,这些请求可能会并行运行。
最后,调用 WorkContinuation.enqueue()
方法对整个工作链执行入队操作。
WorkManager.getInstance(myContext)
.beginWith(workA)
.then(workB)
.then(workC)
.enqueue()
WorkManager.getInstance(myContext)
.beginWith(listOf(workA1, workA2, workA3)) // 这三个 Worker 会并行执行
.then(workB) // 依赖 Worker (只会在前面三个 Worker 执行后才会执行)
.then(listOf(workC1, workC2)) // 依赖 Worker (会在 workB 执行后才会执行)
.enqueue()
WorkManager 会根据每个任务的指定约束,按请求的顺序运行任务。如果有任务返回 Result.failure()
,整个序列结束。
WorkContinuation构建更复杂的链接任务
如需设置更复杂的链接任务,可以使用 WorkContinuation.combine(List<OneTimeWorkRequest>)
方法来联接多个任务链。
例如,假设您要运行像如下序列:
如需设置该序列,请创建两个单独的链,然后将它们联接成第三个链:
val chain1 = WorkManager.getInstance(myContext).beginWith(workA).then(workB)
val chain2 = WorkManager.getInstance(myContext).beginWith(workC).then(workD)
val chain3 = WorkContinuation.combine(Arrays.asList(chain1, chain2)).then(workE)
chain3.enqueue()
在这种情况下,WorkManager
会在 workB
之前运行 workA
,在 workD
之前运行 workC
。然后会在 workB
和 workD
都完成后,运行 workE
。
注意:虽然
WorkManager
会按顺序运行各个子链,但并不保证chain1
中的任务如何与chain2
中的任务重叠。例如,workB
可能会在workC
的前面或后面运行,或者两者也可能会同时运行。唯一可以保证的就是每个子链中的任务将按顺序运行,即workB
会在workA
完成之后再启动。
构建 Unique Worker 唯一工作链
WorkManager 可以创建 Unique Worker 唯一工作链,方法是通过调用 beginUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)
(而非 beginWith(OneTimeWorkRequest)
)。每个 Unique Worker 工作链都有一个名称,WorkManager 不允许同时存在多个具有相同名称的工作链。当您创建新的 Unique Worker 工作链时,如果已存在同名的未完成工作链,您需要指定 WorkManager 应执行什么操作(请参考前面的 冲突解决策略 部分来决定设置哪些冲突策略)。
continuation = WorkManager.getInstance(context)
.beginUniqueWork(
Constants.IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker::class.java)
).thenMaybe<WaterColorFilterWorker>(waterColor)
.thenMaybe<GrayScaleFilterWorker>(grayScale)
.thenMaybe<BlurEffectFilterWorker>(blur)
.then(
if (save) {
workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
} else { // upload
workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
}
)
InputMerger 处理输入冲突
当您链接 OneTimeWorkRequest
实例时,父级工作请求的输出将作为子级的输入传入。例如,在下面的示例中,有 3 个不同的 Worker
并行运行,然后plantName1
、plantName2
和 plantName3
这些 Worker 的结果的输出将作为输入传递给处理缓存的 cache Worker
请求。最后,cache Worker
的输出将传递到 upload Worker
,由 upload Worker
将结果上传到远程服务器。
WorkManager.getInstance(myContext)
.beginWith(listOf(plantName1, plantName2, plantName3))
.then(cache)
.then(upload)
.enqueue()
为了管理来自多个父级工作请求的输入,WorkManager
使用 InputMerger
。
WorkManager
提供两种不同类型的 InputMerger
:
-
OverwritingInputMerger
: 会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。 -
ArrayCreatingInputMerger
:会尝试合并输入,并在必要时创建数组。
如果您有更具体的用例,则可以创建 InputMerger
的子类来编写自己的用例。
OverwritingInputMerger
OverwritingInputMerger
是默认的合并方法。如果合并过程中存在键冲突,键的最新值将覆盖生成的输出数据中的所有先前版本。
例如,如果每个输入的Worker都有一个与其各自变量名称(“plantName1”、“plantName2” 和 “plantName3”)匹配的键,传递给 cache Worker的数据将具有三个键值对:
如果存在冲突,那么最后一个 Worker 将在争用中“取胜”,其值将传递给 cache:
ArrayCreatingInputMerger
假设我们要保留所有 Worker 的输出,则应使用 ArrayCreatingInputMerger
。
val cache: OneTimeWorkRequest = OneTimeWorkRequestBuilder<PlantWorker>()
.setInputMerger(ArrayCreatingInputMerger::class)
.setConstraints(constraints)
.build()
ArrayCreatingInputMerger
将每个键与数组配对。如果每个键都是唯一的,您会得到一系列一元数组:
如果存在任何键冲突,那么所有对应的值会分组到一个数组中:
链接中的 Worker 状态
只要 Worker 成功完成(即返回 Result.success()
),OneTimeWorkRequest
链便会按顺序执行。运行时,Worker 请求可能会失败或被取消,这会对依赖 Worker 请求产生下游影响。
当第一个 OneTimeWorkRequest
被加入 Worker 请求链队列时,所有后续 Worker 请求会被屏蔽,直到第一个 Worker 请求的工作完成为止。
在加入队列且满足所有工作约束后,第一个 Worker 请求开始运行。如果工作在根 OneTimeWorkRequest
或 List<OneTimeWorkRequest>
中成功完成(即返回 Result.success()
),系统会将下一组依赖 Worker 请求加入队列。
只要每个 Worker 请求都成功完成, Worker 请求链中的剩余 Worker 请求就会遵循相同的运行模式,直到链中的所有 Worker 都完成为止。这是最简单的用例,通常也是首选用例,但处理错误状态同样重要。
如果在 Worker 处理工作请求时出现错误,会根据您预定义的退避策略来重试该请求。重试请求链中的某个请求意味着,系统将使用提供给该请求的输入数据仅对该请求进行重试。并行运行的所有其他作业均不会受到影响。
如果未定义重试策略或已用尽,或者以其他方式让 OneTimeWorkRequest
返回 Result.failure()
的某种状态,该 Worker 请求和所有依赖 Worker 请求都会被标记为 FAILED
.
OneTimeWorkRequest
被取消时遵循相同的逻辑。任何依赖 Worker 请求也会被标记为 CANCELLED
,并且无法执行其工作。
请注意,如果要向已失败或已取消 Worker 请求的链附加更多 Worker 请求,新附加的 Worker 请求也会分别标记为 FAILED
或 CANCELLED
。如果您想扩展现有链的工作,请将冲突解决策略 设为ExistingWorkPolicy
中的 APPEND_OR_REPLACE
(参考前面介绍)。
创建 Worker 请求链时,依赖 Worker 请求应定义重试策略,以确保始终及时完成工作。失败的工作请求可能会导致链不完整和/或出现意外状态。
自定义 WorkManager 配置和初始化
默认情况下,当您的应用启动时,WorkManager 使用适合大多数应用的合理选项自动进行配置。如果您需要进一步控制 WorkManager 管理和调度工作的方式,可以通过自行初始化 WorkManager 来自定义 WorkManager 配置。
按需初始化
通过按需初始化,您可以仅在需要 WorkManager 时创建该组件,而不必每次应用启动时都创建。这样做可将 WorkManager 从关键启动路径中移出,从而提高应用启动性能。如需使用按需初始化,请执行以下操作:
-
移除默认初始化程序
如需提供自己的配置,必须先移除默认初始化程序,请使用合并规则
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" />
-
实现 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()
实现才能生效。
在定义自定义 WorkManager
配置后,WorkManager
会在您调用 WorkManager.getInstance(Context)
时进行初始化,而不是在应用启动时自动初始化。
自定义 WorkerFactory
WorkManager
有一个默认的 WorkerFactory
,它在运行时使用反射根据传入的 Worker
的 className
来创建 Worker
的对象实例。
如果你创建了一个WorkRequest
,然后你用一个不同的类名重构了应用程序,WorkManager
将无法找到正确的类,并抛出ClassNotFoundException
。
你可能想要在你的worker构造函数中添加其他参数。例如,下面的 Worker
需要添加一个与远程服务器通信的Retrofit
服务对象的引用:
class UpvoteStoryWorker(
appContext: Context,
workerParams: WorkerParameters,
private val service: UpvoteStoryHttpApi // add
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
return try {
// Upvote story
Result.success()
} catch (e: Exception) {
if (runAttemptCount < MAX_NUMBER_OF_RETRY) {
Result.retry()
} else {
Result.failure()
}
}
}
}
如果我们对应用程序进行了这样的更改,它会正常编译,但是,只要我们执行它,WorkManager
尝试实例化这个CoroutineWorker
类,应用程序就会抛出异常直接崩溃:
Caused by java.lang.NoSuchMethodException: <init> [class android.content.Context, class androidx.work.WorkerParameters]
它提示你找不到正确的init
方法来实例化 Worker
对象。此时,我们需要一个自定义的 WorkerFactory
来解决:
class MyWorkerFactory(private val service: UpvoteStoryHttpApi) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
// This only handles a single Worker, please don’t do this!!
// See below for a better way using DelegatingWorkerFactory
return UpvoteStoryWorker(appContext, workerParameters, DesignerNewsService)
}
}
在添加自定义的 WorkerFactory
到 WorkManager
之前,同样需要按照前面介绍的先去修改AndroidManifest.xml
文件,禁用 WorkManager
默认的初始化配置。
然后将自定义的 WorkerFactory
添加到 Configuration
中:
class MyApplication : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.setWorkerFactory(MyWorkerFactory(DesignerNewsService))
.build()
...
}
如果您的应用程序中只有一个Worker
类型,那么到这里就OK了。如果有多个,或者希望将来有更多,那么更好的解决方案是使用 DelegatingWorkerFactory。
DelegatingWorkerFactory
除了修改 WorkManager
的配置来直接使用我们自定义的工厂类外,我们可以使用一个DelegatingWorkerFactory
,并调用它的 addFactory()
方法向它添加我们自己的WorkerFactory
。然后你可以有多个工厂,每个工厂负责处理一个或多个 Worker
对象。在DelegatingWorkerFactory
中注册你的工厂后,它会处理好协调多个工厂。
在这种情况下,你的工厂类需要注意检查传入的 workerClassName
参数是否能够被正常处理。如果当前工厂类不知道该如何处理传入的 workerClassName
,它将返回null
,并且DelegatingWorkerFactory
将继续去处理下一个注册的工厂。如果所有注册的工厂都不知道该如何处理一个 workerClass 类,那么它将回退到采用使用反射行为的默认工厂来处理。
下面的示例中,如果工厂类不知道如何处理workerClassName
,它将返回null
:
class MyWorkerFactory(private val service: DesignerNewsService) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
return when(workerClassName) {
UpvoteStoryWorker::class.java.name ->
ConferenceDataWorker(appContext, workerParameters, service)
else ->
// Return null, so that the base class can delegate to the default WorkerFactory.
null
}
}
}
然后我们的WorkManager
配置变成下面这样:
class MyApplication : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration {
val myWorkerFactory = DelegatingWorkerFactory().apply {
addFactory(MyWorkerFactory(service))
// Add here other factories that you may need in your application
}
return Configuration.Builder()
.setMinimumLoggingLevel(Log.INFO)
.setWorkerFactory(myWorkerFactory)
.build()
}
...
}
如果您有多个需要不同类型的Worker
,您可以创建第二个工厂,并再次调用addFactory
添加它。
WorkManager 中的线程处理
WorkManager 提供了四种不同类型的 Worker:
- Worker 是最简单的实现, WorkManager 会在后台线程中自动运行该基元(您可以将它替换掉)。
- CoroutineWorker 是为 Kotlin 用户建议的实现。
CoroutineWorker
实例公开了后台工作的一个挂起函数。默认情况下,这些实例运行默认的Dispatcher
,但您可以进行自定义。 - RxWorker 是为
RxJava
用户建议的实现。如果您有很多现有异步代码是用RxJava
建模的,则应使用RxWorker
。 - ListenableWorker 是
Worker
、CoroutineWorker
和RxWorker
的基类。这个类专为需要与基于回调的异步 API(例如 FusedLocationProviderClient)进行交互并且不使用 RxJava 的 Java 开发者而设计。
Worker 中的线程处理
当您使用 Worker
时,WorkManager 会自动在后台线程中调用 Worker.doWork()
。该后台线程来自于 WorkManager 的 Configuration
中指定的 Executor
。默认情况下,WorkManager 会为您设置 Executor
,但您也可以自己进行自定义。例如,您可以在应用中共享现有的后台 Executor
,也可以创建单线程 Executor
以确保所有后台工作都按顺序执行,甚至可以指定一个自定义 Executor
。如需自定义 Executor
,请确保手动初始化 WorkManager。
在手动配置 WorkManager 时,您可以按以下方式指定 Executor
:
WorkManager.initialize( context,
Configuration.Builder()
// Uses a fixed thread pool of size 8 threads.
.setExecutor(Executors.newFixedThreadPool(8))
.build()
)
下面是一个简单的 Worker 示例,其会下载网页内容 100 次:
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): ListenableWorker.Result {
repeat(100) {
try {
downloadSynchronously("https://www.google.com")
} catch (e: IOException) {
return ListenableWorker.Result.failure()
}
}
return ListenableWorker.Result.success()
}
}
请注意,Worker.doWork()
是同步调用,您应以阻塞方式完成整个后台工作,并在方法退出时完成工作。如果您在 doWork()
中调用异步 API 并返回 Result
,那么回调可能无法正常运行。如果您遇到这种情况,请考虑使用 ListenableWorker
。
如果当前正在运行的 Worker 因任何原因而停止,它就会收到对 Worker.onStopped()
的调用。在必要的情况下,只需重写此方法或调用 Worker.isStopped()
,即可对代码进行检查点处理并释放资源。当上述示例中的 Worker
被停止时,内容的下载可能才完成了一半;但即使该工作器被停止,下载也会继续。如需优化此行为,您可以执行以下操作:
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): ListenableWorker.Result {
repeat(100) {
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依赖)。不要继承 Worker
,而应继承 CoroutineWorker
,后者包含 doWork()
的挂起版本。例如,如果要构建一个简单的 CoroutineWorker
来执行某些网络操作,示例代码如下:
class CoroutineDownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val data = downloadSynchronously("https://www.google.com")
saveData(data)
return Result.success()
}
}
请注意,CoroutineWorker.doWork()
是一个挂起函数。此代码不同于 Worker
,不会在 Configuration
中指定的 Executor
中运行,而是默认为 Dispatchers.Default
。您可以提供自己的 CoroutineContext
来自定义这个行为。在上面的示例中,如果希望在 Dispatchers.IO
上完成此操作,可进行如下修改:
class CoroutineDownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
withContext(Dispatchers.IO) {
val data = downloadSynchronously("https://www.google.com")
saveData(data)
return Result.success()
}
}
}
CoroutineWorker
通过取消协程并传播取消信号来自动处理停工情况。您无需执行任何特殊操作来处理停工情况。
在其他进程中运行 CoroutineWorker
如需将 Worker
绑定到 特定进程,可以使用 RemoteCoroutineWorker
(ListenableWorker
的实现)。
RemoteCoroutineWorker
会使用在构建 Worker
请求时于输入数据中提供的两个额外参数绑定到特定进程:ARGUMENT_CLASS_NAME
和 ARGUMENT_PACKAGE_NAME
。
以下示例演示了如何构建绑定到特定进程的 Worker
请求:
val PACKAGE_NAME = "com.example.background.multiprocess"
val serviceName = RemoteWorkerService::class.java.name
val componentName = ComponentName(PACKAGE_NAME, serviceName)
val data: Data = Data.Builder()
.putString(ARGUMENT_PACKAGE_NAME, componentName.packageName)
.putString(ARGUMENT_CLASS_NAME, componentName.className)
.build()
return OneTimeWorkRequest.Builder(ExampleRemoteCoroutineWorker::class.java)
.setInputData(data)
.build()
对于每个 RemoteWorkerService
,需要在 AndroidManifest.xml
文件中添加服务定义:
<manifest ... >
<service
android:name="androidx.work.multiprocess.RemoteWorkerService"
android:exported="false"
android:process=":worker1" />
<service
android:name=".RemoteWorkerService2"
android:exported="false"
android:process=":worker2" />
...
</manifest>
创建和管理长时间运行的 worker
Kotlin 开发者应使用 CoroutineWorker.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 setForeground() 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(notificationId, 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"
}
}
对于 Java 开发者,应使用 ListenableWorker
或 Worker
的 setForegroundAsync()
API,该 API 会返回 ListenableFuture<Void>
。
下面是一个简单的示例,说明了一个下载文件的长时间运行 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 setForegroundAsync(createForegroundInfo(myProgress))
// 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(notificationId, notification);
}
@RequiresApi(Build.VERSION_CODES.O)
private void createChannel() {
// Create a Notification channel
}
}
将前台服务类型添加到长时间运行的 worker
如果应用以 Android 10
(API 级别 29
)或更高版本为目标平台,且包含需要位置信息访问权限的长时间运行的 worker,请指明该 worker 使用 location 的前台服务类型。此外,如果应用以 Android 11
(API 级别 30
)或更高版本为目标平台,且包含需要访问摄像头或麦克风的长时间运行的 worker,则需要分别声明 camera 或 microphone 前台服务类型。
如需添加这些前台服务类型,请完成以下各步骤。
-
在应用清单文件中声明 worker 的前台服务类型
在以下示例中,worker 需要位置信息和麦克风访问权限:
// AndroidManifest.xml
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="location|microphone"
tools:node="merge" />
注意:
tools:node="merge"
会将上述代码段中的<service>
元素声明与WorkManager
自身清单中的SystemForegroundService
定义的声明合并在一起。
-
在运行时指定前台服务类型
当您调用
setForeground()
或setForegroundAsync()
时,请指定前台服务类型FOREGROUND_SERVICE_TYPE_LOCATION
、FOREGROUND_SERVICE_TYPE_CAMERA
或FOREGROUND_SERVICE_TYPE_MICROPHONE
,如以下代码段所示。
// MyLocationAndMicrophoneWorker
private fun createForegroundInfo(progress: String): ForegroundInfo {
// ...
return ForegroundInfo(NOTIFICATION_ID, notification,
FOREGROUND_SERVICE_TYPE_LOCATION or FOREGROUND_SERVICE_TYPE_MICROPHONE)
}
注意:在运行时,您可以选择限制长时间运行的 worker 对数据的访问,方法是传递您已在清单文件中声明的一部分前台服务类型。
调试 WorkManager
启用日志记录
如需启用日志记录功能,请参考前面介绍的自定义初始化部分。首先,你需要修改 AndroidManifest.xml
清单文件使用合并规则 remove
来停用 WorkManager
的默认初始化配置:
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove"/>
在停用WorkManager
默认初始化程序后,在Application
中执行按需初始化:
class MyApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
}
完成此项更改后,WorkManager 将在日志记录为 DEBUG
的前提下运行。更好的选择是仅针对应用的调试 build 对 WorkManager 进行此设置,方法如下:
class BlurApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration {
return if (BuildConfig.DEBUG) {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
} else {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.ERROR)
.build()
}
}
...
}
启用 DEBUG
日志记录后,在 Logcat 控制台中会开始显示更多包含标记前缀 WM-
的日志。
使用 adb shell dumpsys jobscheduler
在 API 级别 23
或更高版本设备上,可以运行命令 adb shell dumpsys jobscheduler
来查看归属于自己应用的作业列表。会输出以下类似的内容:
JOB #u0a172/4: 6412553 com.google.android.youtube/androidx.work.impl.background.systemjob.SystemJobService
u0a172 tag=*job*/com.google.android.youtube/androidx.work.impl.background.systemjob.SystemJobService
Source: uid=u0a172 user=0 pkg=com.google.android.youtube
JobInfo:
Service: com.google.android.youtube/androidx.work.impl.background.systemjob.SystemJobService
Requires: charging=false batteryNotLow=false deviceIdle=false
Extras: mParcelledData.dataSize=180
Network type: NetworkRequest [ NONE id=0, [ Capabilities: NOT_METERED&INTERNET&NOT_RESTRICTED&TRUSTED&VALIDATED Uid: 10172] ]
Minimum latency: +1h29m59s687ms
Backoff: policy=1 initial=+30s0ms
Has early constraint
Required constraints: TIMING_DELAY CONNECTIVITY [0x90000000]
Satisfied constraints: DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED WITHIN_QUOTA [0x3400000]
Unsatisfied constraints: TIMING_DELAY CONNECTIVITY [0x90000000]
Tracking: CONNECTIVITY TIME QUOTA
Implicit constraints:
readyNotDozing: true
readyNotRestrictedInBg: true
Standby bucket: RARE
Base heartbeat: 0
Enqueue time: -51m29s853ms
Run time: earliest=+38m29s834ms, latest=none, original latest=none
Last run heartbeat: 0
Ready: false (job=false user=true !pending=true !active=true !backingup=true comp=true)
使用 WorkManager 时,负责管理 worker 执行的组件为 SystemJobService
(搭载 API 级别 23
或更高版本)。您应该查找归属于您的软件包名称和 androidx.work.impl.background.systemjob.SystemJobService
的作业实例。
对于每个作业,命令的输出都会列出必需、满足和不满足约束条件。您应该检查是否完全满足Worker的约束条件。
您可以利用它检查最近是否调用了 SystemJobService
。输出还包括最近执行的作业的历史记录:
Job history:
-1h35m26s440ms START: #u0a107/9008 com.google.android.youtube/androidx.work.impl.background.systemjob.SystemJobService
-1h35m26s362ms STOP-P: #u0a107/9008 com.google.android.youtube/androidx.work.impl.background.systemjob.SystemJobService app called jobFinished
从 WorkManager 2.4.0 及更高版本请求诊断信息
在应用的调试 build 中,您可以使用以下命令从 WorkManager 2.4.0
及更高版本请求诊断信息:
adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" -p "<your_app_package_name>"
这提供了以下方面的信息:
- 在过去 24 小时内完成的工作请求。
- 目前正在运行的工作请求。
- 预定运行的工作请求。
诊断信息如下所示(输出通过 logcat 显示):
adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" -p "androidx.work.integration.testapp"
adb logcat
...
2020-02-13 14:21:37.990 29528-29660/androidx.work.integration.testapp I/WM-DiagnosticsWrkr: Recently completed work:
2020-02-13 14:21:38.083 29528-29660/androidx.work.integration.testapp I/WM-DiagnosticsWrkr: Id Class Name State Unique Name Tags
08be261c-2def-4bd6-a716-1e4410968dc4 androidx.work.impl.workers.DiagnosticsWorker SUCCEEDED null androidx.work.impl.workers.DiagnosticsWorker
48ce04f1-8df9-450b-96ec-6eceabb9c690 androidx.work.impl.workers.DiagnosticsWorker SUCCEEDED null androidx.work.impl.workers.DiagnosticsWorker
c46f4699-c384-440c-a10e-26d56ce02963 androidx.work.impl.workers.DiagnosticsWorker SUCCEEDED null androidx.work.impl.workers.DiagnosticsWorker
ce125372-046e-484e-949f-9abb35ce62c3 androidx.work.impl.workers.DiagnosticsWorker SUCCEEDED null androidx.work.impl.workers.DiagnosticsWorker
72887ddd-8ed1-4018-b798-fac218e95e16 androidx.work.impl.workers.DiagnosticsWorker SUCCEEDED null androidx.work.impl.workers.DiagnosticsWorker
dcff3d61-320d-4996-8644-5d97944bd09c androidx.work.impl.workers.DiagnosticsWorker SUCCEEDED null androidx.work.impl.workers.DiagnosticsWorker
acab0bf7-6087-43ad-bdb5-be0df9195acb androidx.work.impl.workers.DiagnosticsWorker SUCCEEDED null androidx.work.impl.workers.DiagnosticsWorker
23136bcd-01dd-46eb-b910-0fe8a140c2a4 androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
245f4879-c6d2-4997-8130-e4e90e1cab4c androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
17d05835-bb61-429a-ad11-fe43fc320a54 androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
e95f12be-4b0c-4e64-88da-8ee07a31e42f androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
431c3ec2-4a55-469b-b50b-4072d35f1232 androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
883a388f-f911-4098-9143-37bd8fbc098a androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
b904163c-6822-4299-8d5a-78df49b7e53d androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
453fd7b9-2b16-45b9-abc5-3d2ce7b6a4ba androidx.work.integration.testapp.ToastWorker SUCCEEDED null androidx.work.integration.testapp.ToastWorker
2020-02-13 14:21:38.083 29528-29660/androidx.work.integration.testapp I/WM-DiagnosticsWrkr: Running work:
2020-02-13 14:21:38.089 29528-29660/androidx.work.integration.testapp I/WM-DiagnosticsWrkr: Id Class Name State Unique Name Tags
b87c8a4f-4ac6-4e25-ba3e-4cea53ce468a androidx.work.impl.workers.DiagnosticsWorker RUNNING null androidx.work.impl.workers.DiagnosticsWorker
...
WorkManager 源码解析
WorkManager的初始化
首先 WorkManager 的初始化并不是如下代码:
WorkManager.getInstance(this)
而是在 AndroidManifest.xml
清单文件中配置的 provider
指定的 WorkManagerInitializer
类中:
public final class WorkManagerInitializer implements Initializer<WorkManager> {
private static final String TAG = Logger.tagWithPrefix("WrkMgrInitializer");
@Override
public WorkManager create(@NonNull Context context) {
// Initialize WorkManager with the default configuration.
Logger.get().debug(TAG, "Initializing WorkManager with default configuration.");
WorkManager.initialize(context, new Configuration.Builder().build());
return WorkManager.getInstance(context);
}
@Override
public List<Class<? extends androidx.startup.Initializer<?>>> dependencies() {
return Collections.emptyList();
}
}
在以前的版本中,WorkManagerInitializer
是继承的 ContentProvider
,但是最新的版本中已经改为继承 Initializer
接口,该接口可以负责在App启动过程中初始化各个库。
由于WorkManager是个单例,在此时WorkManager就已经被初始化了,在initialize()
之前,会创建一个默认的Configuration
,Configuration
设置了许多属性,用来管理和调度工作的方式。
看一下 initialize()
的实现,它是定义在WorkManager的子类WorkManagerImpl
中实现的:
public static void initialize(@NonNull Context context, @NonNull Configuration configuration) {
synchronized (sLock) {
......
if (sDelegatedInstance == null) {
context = context.getApplicationContext();
if (sDefaultInstance == null) {
sDefaultInstance = new WorkManagerImpl(
context,
configuration,
new WorkManagerTaskExecutor(configuration.getTaskExecutor()));
}
sDelegatedInstance = sDefaultInstance;
}
}
}
这里如果时 sDefaultInstance
为null
,WorkManager
会先创建一个默认的WorkManagerImpl
对象,并且会创建一个WorkManagerTaskExecutor
对象,用来执行WorkManager
的任务。
看一下 WorkManagerImpl
的构造方法:
// WorkManagerImpl.java
public WorkManagerImpl(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor) {
this(context,
configuration,
workTaskExecutor,
context.getResources().getBoolean(R.bool.workmanager_test_configuration));
}
public WorkManagerImpl(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
boolean useTestDatabase) {
this(context,
configuration,
workTaskExecutor,
WorkDatabase.create(
context.getApplicationContext(),
workTaskExecutor.getBackgroundExecutor(),
useTestDatabase)
);
}
public WorkManagerImpl(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
@NonNull WorkDatabase database) {
Context applicationContext = context.getApplicationContext();
Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
List<Scheduler> schedulers =
createSchedulers(applicationContext, configuration, workTaskExecutor);
Processor processor = new Processor(
context,
configuration,
workTaskExecutor,
database,
schedulers);
internalInit(context, configuration, workTaskExecutor, database, schedulers, processor);
}
在 WorkManagerImpl
构造函数中调用 WorkDatabase.create()
创建了数据库WorkDatabase
对象,WorkDatabase
负责将任务列表序列化到本地,记录每一个任务的属性,执行条件,执行顺序及执行状态等,从而保证任务在冷启动或硬件重启后,能够根据条件继续执行。
WorkManagerImpl
最终调用的构造方法中有三个重要的初始化步骤。
步骤一:createSchedulers()
创建的Schedulers
进行任务调度。
// WorkManagerImpl.java
public List<Scheduler> createSchedulers(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor taskExecutor) {
// return一个Scheduler数组。其中GreedyScheduler()是实例化好了的
return Arrays.asList(
Schedulers.createBestAvailableBackgroundScheduler(context, this),
// Specify the task executor directly here as this happens before internalInit.
// GreedyScheduler creates ConstraintTrackers and controllers eagerly.
new GreedyScheduler(context, configuration, taskExecutor, this)); // 贪婪调度程序
}
步骤二:Processor()
用来管理Schedulers
的执行
public WorkManagerImpl(...) {
...
List<Scheduler> schedulers =
createSchedulers(applicationContext, configuration, workTaskExecutor);
Processor processor = new Processor(
context,
configuration,
workTaskExecutor,
database,
schedulers);
...
}
步骤三:internalInit()
真正的初始化。
// WorkManagerImpl.java
private void internalInit(@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
@NonNull WorkDatabase workDatabase,
@NonNull List<Scheduler> schedulers,
@NonNull Processor processor) {
context = context.getApplicationContext();
mContext = context;
mConfiguration = configuration;
mWorkTaskExecutor = workTaskExecutor;
mWorkDatabase = workDatabase;
mSchedulers = schedulers;
mProcessor = processor;
mPreferenceUtils = new PreferenceUtils(workDatabase);
mForceStopRunnableCompleted = false;
// Check for direct boot mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && context.isDeviceProtectedStorage()) {
throw new IllegalStateException("Cannot initialize WorkManager in direct boot mode");
}
// 检查应用程序强制停止 例如:正在执行任务的时候,手机关机了,或者发生了意外,这里就会重试之前失败的任务
mWorkTaskExecutor.executeOnBackgroundThread(new ForceStopRunnable(context, this));
}
WorkManager的初始化总结:
- ① WorkManager的初始化是由WorkManagerInitializer这个类负责的,实现类是WorkManagerImpl
- ② 会初始化 Configuration,WorkManagerTaskExecutor,WorkDatabase,Schedulers,Processor
- ③ 创建 GreedyScheduler 调度器等
- ④ 发现未完成的,需要重新执行的任务(之前意外中断的继续执行)
WorkManager 的非约束条件任务的执行源码分析
WorkManager.getInstance(this).enqueue(request)
进入enqueue()
源码:
// WorkManager.java
public final Operation enqueue(@NonNull WorkRequest workRequest) {
return enqueue(Collections.singletonList(workRequest));
}
public abstract Operation enqueue(@NonNull List<? extends WorkRequest> requests);
// WorkManagerImpl.java
public Operation enqueue(@NonNull List<? extends WorkRequest> requests) {
// This error is not being propagated as part of the Operation, as we want the
// app to crash during development. Having no workRequests is always a developer error.
if (requests.isEmpty()) {
throw new IllegalArgumentException("enqueue needs at least one WorkRequest.");
}
return new WorkContinuationImpl(this, requests).enqueue();
}
这里创建了一个WorkContinuationImpl
对象,再执行enqueue()
方法。WorkContinuationImpl
是WorkContinuation
的子类:
public WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
@Nullable String name,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<? extends WorkRequest> work,
@Nullable List<WorkContinuationImpl> parents) {
mWorkManagerImpl = workManagerImpl;
mName = name;
mExistingWorkPolicy = existingWorkPolicy;
mWork = work;
mParents = parents;
mIds = new ArrayList<>(mWork.size());
mAllIds = new ArrayList<>();
if (parents != null) {
for (WorkContinuationImpl parent : parents) {
mAllIds.addAll(parent.mAllIds);
}
}
for (int i = 0; i < work.size(); i++) {
String id = work.get(i).getStringId();
mIds.add(id);
mAllIds.add(id);
}
}
WorkContinuationImpl
构造方法中会保存任务相关的所有信息,比如 WorkManager
,WorkRequest
,父WorkContinuation
… 再接着继续看WorkContinuationImpl
的enqueue()
方法的实现:
// WorkContinuationImpl.java
@Override
public @NonNull Operation enqueue() {
// Only enqueue if not already enqueued.
if (!mEnqueued) {
// The runnable walks the hierarchy of the continuations
// and marks them enqueued using the markEnqueued() method, parent first.
EnqueueRunnable runnable = new EnqueueRunnable(this);
mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(runnable);
mOperation = runnable.getOperation();
} else {
Logger.get().warning(TAG,
String.format("Already enqueued work ids (%s)", TextUtils.join(", ", mIds)));
}
return mOperation;
}
这里可以看出,enqueue
方法提交的任务会交给EnqueueRunnable
执行,这里主要是创建了一个 EnqueueRunnable
对象丢到调度器中去执行。
看一下 EnqueueRunnable
的 run
方法源码:
// EnqueueRunnable.java
@Override
public void run() {
try {
if (mWorkContinuation.hasCycles()) {
throw new IllegalStateException(
String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
}
// addToDatabase()的作用是把WorkSpec存入到数据库,并对任务的状态进行校验
boolean needsScheduling = addToDatabase();
if (needsScheduling) {
// Enable RescheduleReceiver, only when there are Worker's that need scheduling.
final Context context =
mWorkContinuation.getWorkManagerImpl().getApplicationContext();
PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
scheduleWorkInBackground(); // 重点
}
mOperation.setState(Operation.SUCCESS);
} catch (Throwable exception) {
mOperation.setState(new Operation.State.FAILURE(exception));
}
}
public void scheduleWorkInBackground() {
WorkManagerImpl workManager = mWorkContinuation.getWorkManagerImpl();
// 重点
Schedulers.schedule(
workManager.getConfiguration(),
workManager.getWorkDatabase(),
workManager.getSchedulers());
}
在EnqueueRunnable的run方法中,会将入队的任务信息入库操作(使用Room组件),这就是为什么即使App被杀死了也会执行到任务。
而最终调用了 Schedulers.schedule()
方法进行调度,传入了Configuration
, WorkDatabase
, Scheduler
这三个对象。
看一下 schedule()
函数:
// Schedulers.java
public static void schedule(Configuration configuration, WorkDatabase workDatabase, List<Scheduler> schedulers) {
......
workDatabase.beginTransaction();
try {
......
if (eligibleWorkSpecsForLimitedSlots != null
&& eligibleWorkSpecsForLimitedSlots.size() > 0) {
......
for (WorkSpec workSpec : eligibleWorkSpecsForLimitedSlots) {
// 先进行了一系列的数据库操作,然后开始根据条件每个任务进行调度,更新保存数据库等操作
workSpecDao.markWorkSpecScheduled(workSpec.id, now);
}
}
workDatabase.setTransactionSuccessful();
}
......
if (eligibleWorkSpecsForLimitedSlots != null
&& eligibleWorkSpecsForLimitedSlots.size() > 0) {
......
for (Scheduler scheduler : schedulers) {
if (scheduler.hasLimitedSchedulingSlots()) {
// scheduler.schedule()对每个任务进行调度处理
scheduler.schedule(eligibleWorkSpecsArray);
}
}
}
if (allEligibleWorkSpecs != null && allEligibleWorkSpecs.size() > 0) {
......
for (Scheduler scheduler : schedulers) {
if (!scheduler.hasLimitedSchedulingSlots()) {
scheduler.schedule(enqueuedWorkSpecsArray);
}
}
}
}
上面代码中会调用scheduler.schedule()
对每个任务进行调度处理,我们是没有约束的一次性任务,所以看一下GreedyScheduler
对于schedule()
方法的实现:
// GreedyScheduler.java
@Override
public void schedule(@NonNull WorkSpec... workSpecs) {
......
for (WorkSpec workSpec : workSpecs) {
......
if (workSpec.state == WorkInfo.State.ENQUEUED) {
if (now < nextRunTime) {
// Future work
if (mDelayedWorkTracker != null) {
mDelayedWorkTracker.schedule(workSpec);
}
} else if (workSpec.hasConstraints()) {
if (SDK_INT >= 23 && workSpec.constraints.requiresDeviceIdle()) {
......
} else if (SDK_INT >= 24 && workSpec.constraints.hasContentUriTriggers()) {
......
} else {
constrainedWorkSpecs.add(workSpec);
constrainedWorkSpecIds.add(workSpec.id);
}
} else {
......
// 注意:由于无约束条件的会走这里
mWorkManagerImpl.startWork(workSpec.id);
}
}
}
......
}
继续看WorkManagerImpl.startWork()
方法:
public void startWork(@NonNull String workSpecId) {
startWork(workSpecId, null);
}
public void startWork(String workSpecId, WorkerParameters.RuntimeExtras runtimeExtras) {
mWorkTaskExecutor.executeOnBackgroundThread(
new StartWorkRunnable(this, workSpecId, runtimeExtras));
}
可以看到创建了一个 StartWorkRunnable
对象丢入调度器执行。
// StartWorkRunnable.java
@Override
public void run() {
mWorkManagerImpl.getProcessor().startWork(mWorkSpecId, mRuntimeExtras);
}
而 StartWorkRunnable
的run
方法是交给了 WorkManagerImpl
的 Processor
对象去处理:
// Processor.java
public boolean startWork(String id, WorkerParameters.RuntimeExtras runtimeExtras) {
WorkerWrapper workWrapper;
synchronized (mLock) {
if (isEnqueued(id)) {
return false;
}
workWrapper =
new WorkerWrapper.Builder(
mAppContext,
mConfiguration,
mWorkTaskExecutor,
this,
mWorkDatabase,
id)
.withSchedulers(mSchedulers)
.withRuntimeExtras(runtimeExtras)
.build();
ListenableFuture<Boolean> future = workWrapper.getFuture();
future.addListener(
new FutureListener(this, id, future),
mWorkTaskExecutor.getMainThreadExecutor());
mEnqueuedWorkMap.put(id, workWrapper);
}
// 将封装的workWrapper 对象丢到调度器中执行
mWorkTaskExecutor.getBackgroundExecutor().execute(workWrapper);
return true;
}
下面进入WorkerWrapper
类看看WorkerWrapper的run()
方法的实现:
// WorkerWrapper.java
@Override
public void run() {
mTags = mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId);
mWorkDescription = createWorkDescription(mTags);
runWorker();
}
private void runWorker() {
......
final WorkerParameters params = new WorkerParameters(.....);
if (mWorker == null) {
// 调用默认的工厂类反射创建Worker对象
mWorker = mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
mAppContext,
mWorkSpec.workerClassName,
params);
}
......
if (trySetRunning()) {
final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
final WorkForegroundRunnable foregroundRunnable =
new WorkForegroundRunnable(......);
mWorkTaskExecutor.getMainThreadExecutor().execute(foregroundRunnable);
final ListenableFuture<Void> runExpedited = foregroundRunnable.getFuture();
runExpedited.addListener(new Runnable() {
@Override
public void run() {
try {
runExpedited.get();
// 这里调用了mWorker.startWork()方法开始执行Worker的任务
mInnerFuture = mWorker.startWork();
future.setFuture(mInnerFuture);
} catch (Throwable e) {
future.setException(e);
}
}
}, mWorkTaskExecutor.getMainThreadExecutor());
}
......
}
继续看 Worker.startWork()
方法:
// Worker.java
@Override
public final @NonNull ListenableFuture<Result> startWork() {
mFuture = SettableFuture.create();
getBackgroundExecutor().execute(new Runnable() {
@Override
public void run() {
try {
Result result = doWork(); // 调用 doWork() 开始干活
mFuture.set(result);
} catch (Throwable throwable) {
mFuture.setException(throwable);
}
}
});
return mFuture;
}
可以看到这里 Worker.startWork()
方法中最终在后台调度器中执行了我们定义的Worker
对象的doWork()
开始执行任务。
WorkManager的 无约束的任务的执行源码总结:
- WorkManager执行了enqueue()后,创建WorkContinuationImpl对象执行enqueue()方法。
- WorkContinuationImpl持有的EnqueueRunnable对象将任务添加到db,并交给Schedulers去调度。
- Schedulers将任务交给每一个Scheduler去处理,GreedyScheduler会先处理这个任务。
- GreedyScheduler经过一系列判断后,调用WorkManager的startWork()方法执行这种一次性,非延迟,无约束的任务。
- WorkManager持有的StartWorkRunnable对象会将任务交给Processor去处理,执行startWork()方法。
- Processor创建一个WorkerWrapper对象,由它去调用Worker的startWork()方法,进而调用doWork()方法。
WorkManager的有约束条件任务的执行源码分析
约束条件会在清单文件中注册对应的广播接收器,以低电量为例:
<receiver android:directBootAware="false" android:enabled="false"
android:exported="false"
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy">
<intent-filter>
<action android:name="android.intent.action.BATTERY_OKAY"/>
<action android:name="android.intent.action.BATTERY_LOW"/>
</intent-filter>
</receiver>
当在电量变化时,收到BATTERY_LOW
的广播,会在BatteryNotLowProxy
的onReceive()
进行处理:
abstract class ConstraintProxy extends BroadcastReceiver {
private static final String TAG = Logger.tagWithPrefix("ConstraintProxy");
@Override
public void onReceive(Context context, Intent intent) {
Logger.get().debug(TAG, String.format("onReceive : %s", intent));
Intent constraintChangedIntent = CommandHandler.createConstraintsChangedIntent(context);
context.startService(constraintChangedIntent);
}
/**
* Proxy for Battery Not Low constraint
*/
public static class BatteryNotLowProxy extends ConstraintProxy {
}
/**
* Proxy for Battery Charging constraint
*/
public static class BatteryChargingProxy extends ConstraintProxy {
}
/**
* Proxy for Storage Not Low constraint
*/
public static class StorageNotLowProxy extends ConstraintProxy {
}
/**
* Proxy for Network State constraints
*/
public static class NetworkStateProxy extends ConstraintProxy {
}
}
可以看到 ConstraintProxy
总共有四个子类,分别对应四种约束类型。在onReceive()
中会调用createConstraintsChangedIntent()
创建一个SystemAlarmService
的Intent
, 然后启动该Service。
static Intent createConstraintsChangedIntent(@NonNull Context context) {
Intent intent = new Intent(context, SystemAlarmService.class);
intent.setAction(ACTION_CONSTRAINTS_CHANGED);
return intent;
}
看一下 SystemAlarmService
的onStartCommand()
方法:
// SystemAlarmService.java
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
......
if (intent != null) {
mDispatcher.add(intent, startId);
}
return Service.START_REDELIVER_INTENT;
}
这里调用了 SystemAlarmDispatcher
的 add
方法:
// SystemAlarmDispatcher.java
public boolean add(@NonNull final Intent intent, final int startId) {
......
intent.putExtra(KEY_START_ID, startId);
synchronized (mIntents) {
boolean hasCommands = !mIntents.isEmpty();
mIntents.add(intent);
if (!hasCommands) {
processCommand();
}
}
return true;
}
这里又会调用 processCommand()
方法:
// SystemAlarmDispatcher.java
private void processCommand() {
......
try {
processCommandLock.acquire();
// Process commands on the background thread.
mWorkManager.getWorkTaskExecutor().executeOnBackgroundThread(new Runnable() {
@Override
public void run() {
......
if (mCurrentIntent != null) {
......
try {
......
// 重点
mCommandHandler.onHandleIntent(mCurrentIntent, startId,
SystemAlarmDispatcher.this);
} catch (Throwable throwable) {
...
}
}
}
});
}
......
}
这里会调用 mCommandHandler.onHandleIntent()
方法:
// CommandHandler.java
void onHandleIntent(Intent intent, int startId, SystemAlarmDispatcher dispatcher) {
String action = intent.getAction();
if (ACTION_CONSTRAINTS_CHANGED.equals(action)) { // 约束条件发生变化
handleConstraintsChanged(intent, startId, dispatcher);
}
......
}
private void handleConstraintsChanged(Intent intent, int startId, SystemAlarmDispatcher dispatcher) {
ConstraintsCommandHandler changedCommandHandler =
new ConstraintsCommandHandler(mContext, startId, dispatcher);
changedCommandHandler.handleConstraintsChanged();
}
// ConstraintsCommandHandler.java
void handleConstraintsChanged() {
List<WorkSpec> candidates = mDispatcher.getWorkManager().getWorkDatabase()
.workSpecDao()
.getScheduledWork();
ConstraintProxy.updateAll(mContext, candidates);
......
for (WorkSpec workSpec : eligibleWorkSpecs) {
String workSpecId = workSpec.id;
Intent intent = CommandHandler.createDelayMetIntent(mContext, workSpecId);
// 交给 SystemAlarmDispatcher
mDispatcher.postOnMainThread(
new SystemAlarmDispatcher.AddRunnable(mDispatcher, intent, mStartId));
}
mWorkConstraintsTracker.reset();
}
其中 CommandHandler.createDelayMetIntent()
代码如下:
static Intent createDelayMetIntent(@NonNull Context context, @NonNull String workSpecId) {
Intent intent = new Intent(context, SystemAlarmService.class);
intent.setAction(ACTION_DELAY_MET);
intent.putExtra(KEY_WORKSPEC_ID, workSpecId);
return intent;
}
这里CommandHandler.createDelayMetIntent()
方法会创建一个action
为ACTION_DELAY_MET
的Intent
,然后该Intent
交给了 SystemAlarmDispatcher
的AddRunnable
:
static class AddRunnable implements Runnable {
private final SystemAlarmDispatcher mDispatcher;
...
@Override
public void run() {
mDispatcher.add(mIntent, mStartId);
}
}
这里发现又回到了前面分析过的 SystemAlarmDispatcher
的 add
方法中。有点类似一个状态机循环。然后又会循环调用 processCommand() -> mCommandHandler.onHandleIntent()
:
// CommandHandler.java
void onHandleIntent(Intent intent, int startId, SystemAlarmDispatcher dispatcher) {
String action = intent.getAction();
if (ACTION_CONSTRAINTS_CHANGED.equals(action)) {
handleConstraintsChanged(intent, startId, dispatcher);
} else if (ACTION_RESCHEDULE.equals(action)) {
handleReschedule(intent, startId, dispatcher);
} else {
Bundle extras = intent.getExtras();
if (!hasKeys(extras, KEY_WORKSPEC_ID)) {
......
} else {
if (ACTION_SCHEDULE_WORK.equals(action)) {
handleScheduleWorkIntent(intent, startId, dispatcher);
} else if (ACTION_DELAY_MET.equals(action)) { // 这里匹配了 ACTION_DELAY_MET
handleDelayMet(intent, startId, dispatcher); // 重点
}
.....
}
}
}
handleDelayMet()
源码如下:
// CommandHandler.java
private void handleDelayMet(Intent intent, int startId, SystemAlarmDispatcher dispatcher) {
Bundle extras = intent.getExtras();
synchronized (mLock) {
String workSpecId = extras.getString(KEY_WORKSPEC_ID);
if (!mPendingDelayMet.containsKey(workSpecId)) {
DelayMetCommandHandler delayMetCommandHandler =
new DelayMetCommandHandler(mContext, startId, workSpecId, dispatcher);
mPendingDelayMet.put(workSpecId, delayMetCommandHandler);
delayMetCommandHandler.handleProcessWork(); // 重点
} else {
......
}
}
}
这里会调用 DelayMetCommandHandler
的 handleProcessWork()
方法
// DelayMetCommandHandler.java
void handleProcessWork() {
mWakeLock = WakeLocks.newWakeLock(
mContext,
String.format("%s (%s)", mWorkSpecId, mStartId));
mWakeLock.acquire();
WorkSpec workSpec = mDispatcher.getWorkManager()
.getWorkDatabase()
.workSpecDao()
.getWorkSpec(mWorkSpecId);
......
mHasConstraints = workSpec.hasConstraints();
if (!mHasConstraints) {
// 重点
onAllConstraintsMet(Collections.singletonList(mWorkSpecId));
} else {
// Allow tracker to report constraint changes
mWorkConstraintsTracker.replace(Collections.singletonList(workSpec));
}
}
于是来到了 onAllConstraintsMet()
方法:
// DelayMetCommandHandler.java
@Override
public void onAllConstraintsMet(@NonNull List<String> workSpecIds) {
......
synchronized (mLock) {
if (mCurrentState == STATE_INITIAL) {
mCurrentState = STATE_START_REQUESTED;
// 执行 Processor 的 startWork() 方法
boolean isEnqueued = mDispatcher.getProcessor().startWork(mWorkSpecId);
......
}
}
}
这里调用了 mDispatcher.getProcessor().startWork(mWorkSpecId)
, 这里mDispatcher
是SystemAlarmDispatcher
,其 getProcessor()
方法返回的就是WorkManagerImpl
的 Processor
对象。
再继续跟下去就跟前面非约束条件的任务执行流程类似, Processor
最终会走 WorkerWrapper.runWorker() -> Worker.startWork() -> Worker.doWork()
所以殊途同归。
WorkManager的有约束条件任务的执行总结:
管理设备唤醒状态
使屏幕保持开启状态
某些应用需要使屏幕保持开启状态,例如游戏或电影应用。要实现此目标,最好的方法是在您的 Activity 中(仅在 Activity 中,切勿在服务或其他应用组件中)使用 FLAG_KEEP_SCREEN_ON
。例如:
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
这种方法的优点是,与唤醒锁定不同,它不需要特殊权限,并且平台可以正确管理用户在不同应用之间的切换,您的应用无需担心释放未使用的资源。
实现此目标的另一种方法是,在应用的布局 XML 文件中,使用 android:keepScreenOn 属性:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
...
</RelativeLayout>
使用 android:keepScreenOn="true"
功效等同于 FLAG_KEEP_SCREEN_ON
。您可以使用最适合您的应用的任意一种方法。在 Activity 中以编程方式设置标记的优势在于,您可以选择稍后以编程方式清除该标记,从而使屏幕可以关闭。
使 CPU 保持运行状态
如果您需要使 CPU 保持运行状态,以便在设备进入休眠模式之前完成某项工作,可以使用一项称为“唤醒锁定”的 PowerManager
系统服务功能。唤醒锁定可使应用控制主机设备的电源状态。
创建和持有唤醒锁定会对主机设备的电池续航时间产生重大影响。因此,您应仅在绝对必要时使用唤醒锁定,并持有尽可能短的时间。例如,您绝不需要在 Activity 中使用唤醒锁定。如上所述,如果您希望屏幕在 Activity 中保持开启状态,请使用 FLAG_KEEP_SCREEN_ON
。
使用唤醒锁定的一种合理情形是,某项后台服务需要获取唤醒锁定,以便 CPU 在屏幕关闭时保持运行状态,可以完成相关工作。再次声明,由于这种做法会影响电池续航时间,因此应尽量减少其使用频率。
如需使用唤醒锁定,首先要将 WAKE_LOCK
权限添加到应用的清单文件中:
<uses-permission android:name="android.permission.WAKE_LOCK" />
如果您的应用包含使用服务来完成相关工作的广播接收器,您可以使用 WakefulBroadcastReceiver
管理唤醒锁定。这是首选方法。如果您的应用未采用该模式,您可以使用以下方法直接设置唤醒锁定:
val wakeLock: PowerManager.WakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag").apply {
acquire()
}
}
要释放唤醒锁定,请调用 wakelock.release()
。这会释放您对 CPU 的声明。请务必在应用结束对唤醒锁定的使用后立即将其释放,以避免消耗电池电量。