现代 WorkManager API 已发布
随着设备性能提升和软件生态发展,越来越多的 Android 应用需要执行相对更复杂的网络、异步和离线等任务。例如用户想要离线观看某个视频,又不想一直停留在应用界面等待下载完成,那么就需要以一定的方式让这些离线的过程在后台运行。再比如您想将一段精彩的 Vlog 分享到社交媒体,肯定也会希望视频上传时不会影响到自己继续使用设备。这就涉及到了我们今天分享的主题: 使用 WorkManager 管理后台和前台工作。
△ 现代 WorkManager API 已发布
Bilibili 视频链接
https://www.bilibili.com/video/BV16S4y1r7o9/
本文将着重探讨 WorkManager 的 API 以及用法,帮助您深入了解它的运行机制,以及在实际开发中的使用方式。近期也将会有另一篇关于在 Android Studio 中如何更好地使用 WorkManager 的文章,敬请关注。
WorkManager 基础 API
从首个稳定版本发布以来,WorkManager 提供了一些基础 API,帮助您定义工作、放入队列、依次执行,且在工作完成时通知您的应用。以功能划分分类,这些基础 API 包括:
延迟执行
最初的版本中,这些工作只能被定义为延迟执行,也就是它们会在定义之后延期再开始执行。通过这种延期执行策略,一些不紧急或优先级不高的任务将会推后执行。
WorkManager 的延期执行会充分考虑设备的低电耗状态,以及应用的待机存储分区,因此您不必考虑工作需要在哪个具体时间被执行,这些都交给 WorkManager 考虑即可。
工作约束
WorkManager 支持对给定工作运行设定约束条件,约束可确保将工作延迟到满足最佳条件时运行。例如,仅在设备采用不按流量计费的网络连接时、当设备处于空闲状态或者有足够的电量时运行。您可以专心开发应用的其他功能,将对工作条件的检查交给 WorkManager。
约束
https://developer.android.google.cn/topic/libraries/architecture/workmanager/how-to/define-work#work-constraints
工作间的依赖关系
https://developer.android.google.cn/reference/androidx/work/Worker
多次执行的工作
很多具备与服务器同步功能的应用都具有这样的特点: 应用与后端服务器的同步往往不是一次性的,它可能是需要多次执行的。比如当您的应用提供在线编辑服务时,一定需要频繁将本地的编辑数据同步到云端,这就产生了定期执行的工作。
工作状态
由于您可以随时检查某个工作的状态,因此对于定期执行的工作而言,整个生命周期是透明的。您可以知道一个工作是处于队列等待、运行中、阻塞还是已完成状态。
WorkManager 现代 API
上述的基础 API 早在我们发布 WorkManager 的第一个稳定版时就已经提供了。首次在 Android 开发者峰会中谈到 WorkManager 时,我们把它看作是管理可延期后台工作的一个库。如今从底层的角度来看,这种观点仍然是成立的。但后来我们又添加了更多新功能,并让 API 更符合现代规范。
立即执行
现在,当您的应用处于前台时,您可以请求立即执行某项工作。随后即便应用被置于后台,这项工作也不会被中断,而是继续进行。所以,即使用户切换到别的应用去使用,您的应用仍然可以继续实现为照片添加滤镜、保存到本地、上传等一系列工作。
对于大型应用的开发商来说,他们需要在优化资源使用方面投入更多的资源和精力。但 WorkManager 可以凭借优秀的资源分配策略大大减轻他们的负担。
多进程 API
由于使用了新的多进程库处理工作,WorkManager 引入了新的 API,并进行了底层优化来帮助大型应用更有效地安排和执行工作。这得益于新的 WorkManager 可以在一个独立的进程中更高效地进行调度和处理。
强化的工作测试 API
应用发布到商店或是分发给用户之前,测试是非常重要的一个环节。因此我们增加了 API 来帮助您测试单独的 Worker 或是一组具备依赖关系的 Worker。
工具改进
在发布库的同时,我们还改进了众多开发者工具。作为开发者,您可以直接使用 Android Studio 来访问详尽的调试日志和检查信息。
开始使用 WorkManager
这些新引入的 API 和改进的工具在为开发者提供更大便利的同时,也促使我们重新思考使用 WorkManager 的最佳时机。虽然从技术角度,我们设计 WorkManager 的核心思想仍然是正确的,但对于日益复杂的开发生态而言,WorkManager 的能力已经大大超过我们的设计预期。
工作的 "持久化" 特性
WorkManager 可以处理您指派给它的任何类型的工作,因此它已经进化成了一个专门处理任务且值得信赖的好工具。WorkManager 在全局作用域中执行您定义的 Worker,这意味着只要您的应用还在运行,不论是设备方向的变化,还是 Activity 被回收等,您的工作会被一直留存。不过单凭这一点,还不能称之拥有 "持久化" 特性,因此 WorkManager 在底层还使用了 Room 数据库来保证当进程被结束或设备重启后,您的工作仍然可以执行,并有可能从中断位置继续执行。
执行需要长时间运行的工作
WorkManager 2.3 版本引入了对长时间运行的工作的支持。当我们谈到长时间运行的工作时,指的是运行时间超过 10 分钟执行窗口期的工作。通常情况下,一个 Worker 的执行窗口期被限定为 10 分钟。为了能实现长时间运行的工作,WorkManager 将 Worker 的生命周期与前台服务的生命周期捆绑在一起。JobScheduler 和进程内调度程序 (In-Process Scheduler) 仍然能感知到这种工作的存在。
由于前台服务掌握着工作执行的生命周期,而前台服务又需要向用户展示通知信息,所以我们向 WorkManager 添加了相关的 API。用户的注意力持续时间是有限的,所以 WorkManager 提供了 API 让用户能方便地通过通知停止长时间运行的工作。我们来分析一个长时间运行工作示例,代码如下:
class DownloadWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
fun notification(progress: String): Notification = TODO()
// notification 方法根据进度信息生成一条 Android 通知消息。
suspend fun download(inputUrl: String,
outputFile: String,
callback: suspend (progress: String) -> Unit) = TODO()
// 定义一个用于分块下载的方法
fun createForegroundInfo(progress: String): ForegroundInfo {
return ForegroundInfo(id, notification(progress))
}
override suspend fun doWork(): Result {
download(inputUrl, outputFile) { progress ->
val progress = "Progress $progress %"
setForeground(createForegroundInfo(progress))
} // 提供了一个 suspend 标记的 doWork 方法,其中调用下载方法,并显示最新进度信息。
return Result.success()
} //下载完成后,Worker 只需要返回成功即可
}
△ DownloadWorker 类
这里有一个 DownloadWorker 类,它扩展自 CoroutineWorker 类。我们会在这个类当中定义一些辅助方法来简化我们的工作。首先是一个 notification 方法,它可以根据所给定的进度信息生成一条 Android 通知消息。接下来我们要定义一个用于分块下载的方法,这个方法接受三个参数: 下载文件的 URL、文件保存的本地位置、suspend 回调函数。每当某个分块下载状态变化时,此回调就会被执行一次。于是,回调中携带的信息就可以被用来生成一条通知。
CoroutineWorker
https://developer.android.google.cn/reference/kotlin/androidx/work/CoroutineWorker.html
有了这些辅助方法,我们就可以将 WorkManager 执行长时间运行工作所需要的 ForegroundInfo 实例保存起来。ForegroundInfo 是由通知 ID 和通知实例组合构造而成的,请继续参照上述 CoroutineWorker 类的代码示例。
在这段代码里,我们提供了一个 suspend 标记的 doWork 方法,其中调用了刚才提到的分块下载辅助方法。由于每次回调发生时都会提供一些最新的进度信息,所以我们可以利用这些信息来构建通知,并调用 setForeground 方法来向用户显示这些通知。这里调用 setForeground 的操作正是导致 Worker 长时间运行的原因。下载完成后,Worker 只需要返回成功即可,随后 WorkManager 会将 Worker 的执行与前台服务解耦分离、清理通知消息,并在必要时结束相关的服务。因此我们的 Worker 本身并不需要执行服务管理工作。
终止已提交执行的工作
用户可能会突然改变主意,比如想要取消某个工作。某个前台运行服务的通知是无法简单滑动取消的,此前的做法是为这条通知消息添加一个动作,当用户点击时会向 WorkManager 发送一个信号,从而按照用户的意图终止某项工作。您也可以通过执行加急工作来终止,详见后文。
fun notification(progress: String): Notification {
val intent = WorkManager.getInstance(context)
.createCancelPendingIntent(getId())
return NotificationCompat.Builder(applicationContext, id)
.setContentTitle(title)
.setContentText(progress)
// 其他一些操作
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build()
}
△ 派生自 CoroutineWorker 类的 DownloadWorker 类
执行加急工作
https://developer.android.google.cn/jetpack/androidx/releases/work#2.7.0
class SendMessageWorker(context: Context, parameters: WorkerParameters):
CoroutineWorker(context, parameters) {
override suspend fun getForegroundInfo(): ForegroundInfo {
TODO()
}
override suspend fun doWork(): Result {
TODO()
}
}
例如,一个同步聊天应用消息的案例使用了加急工作 API。SendMessageWorker 类扩展自 CoroutineWorker,而它的作用是负责从后台为聊天应用同步消息。加急工作需要在某个前台服务的上下文中运行,这很类似于 Android 12 之前版本中的长时间运行的工作。因此我们的 Worker 类还需要实现 getForegroundInfo 接口,方便生成和显示通知消息。但是在 Android 12 上 WorkManager 不会显示其他的通知,这是因为我们定义的 Worker 背后是由加急作业实现的。您需要像平常那样实现一个 suspend 标记的 doWork 方法。需要注意的是,当您的应用占用了全部的配额后,加急作业可能会被中断。因此我们的 Worker 最好能跟踪某些状态,以便在重新安排执行时间后能够恢复运行。
val request = OneTimeWorkRequestBuilder<ForegroundWorker>()
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
.build()
WorkManager.getInstance(context)
.enqueue(request)
您可以使用 setExpedited API 来安排加急工作,这个 API 会告诉 WorkManager,用户认为给定的工作请求非常重要。由于所能安排的工作存在配额限制,所以您需要表明当应用的配额用尽时该怎么处理,有两种备选方案: 其一是将加急请求变成常规工作请求,其二是在配额耗尽时放弃新的工作请求。
WorkManager 多进程 API
从 2.5 版本开始,WorkManager 对支持多进程的应用进行了若干项改进。如果您需要使用多进程 API,就需要定义 work-multiprocess 工件的依赖项,多进程 API 的目标是在辅助进程中对 WorkManager 的冗余部分或高开销部分进行大范围初始化操作。比如有多个进程在同时获取统一底层 SQLite 数据库的事务锁,这时就会发生 SQLite 争用;而这种争用正是我们想要通过多进程 API 减少的。另一方面,我们还想确保进程内调度程序在正确的进程中运行。
为了解 WorkManager 初始化时哪些部分是冗余的,我们需要清楚它会在后台执行哪些操作。
单进程的初始化
首先观察一下单进程初始化过程。应用启动后,第一件事是有平台调用 Application.onCreate 方法。随后在进程生命周期的某个时间点,WorkManager.getInstance 会被调用以启动 WorkManager 的初始化。当 WorkManager 初始化完毕后,我们运行 ForceStopRunnable。这个过程很重要,因为此时 WorkManager 会检查应用之前是否被强制停止过,它会比较 WorkManager 存储的信息与 JobScheduler 或 AlarmManager 中的信息,确保作业都被准确编入执行计划中。同时,我们也可以重新安排此前中断的某些工作,比如进程崩溃后进行的一些恢复工作。大家都知道,这样做的开销非常高,我们需要在多个子系统中比较和协调状态,但是理想状态下,这种操作只应该被执行一次。另外需要注意,进程内调度程序只在默认进程中运行。
JobScheduler
https://developer.android.google.cn/reference/android/app/job/JobScheduler.htmlAlarmManager
https://developer.android.google.cn/reference/android/app/AlarmManager
多进程的初始化
val config = Configuration.Builder()
.setDefaultProcessName("com.example.app")
.build()
△ 指定应用的默认进程示例代码
通过 WorkManager 定义主进程
val request = OneTimeWorkRequestBuilder<DownloadWorker>()
.build()
RemoteWorkManager.getInstance(context)
.enqueue(request)
<!-- AndroidManifest.xml -->
<service
android:name="androidx.work.multiprocess.RemoteWorkManagerService"
android:exported="false" />
△ Manifest 注册服务示例代码
不同进程中运行 Worker
public class IndexingWorker(
context: Context,
parameters: WorkerParameters
): RemoteCoroutineWorker(context, parameters) {
override suspend fun doRemoteWork(): Result {
doSomething()
return Result.success()
}
}
由于这个方法是在辅助进程中执行的,我们仍然要定义 Worker 需要与哪个进程绑定。为此,我们还需要在 Android Manifest RXML 中添加一个条目。一个应用可以定义多项 RemoteWorker 服务,每项服务都在独立的进程中运行。
<!-- AndroidManifest.xml -->
<service
android:name="androidx.work.multiprocess.RemoteWorkerService"
android:exported="false"
android:process=":background" />
△ Manifest 注册服务示例代码
val inputData = workDataOf(
ARGUMENT_PACKAGE_NAME to context.packageName,
ARGUMENT_CLASS_NAME to RemoteWorkerService::class.java.name
)
val request = OneTimeWorkRequestBuilder<RemoteDownloadWorker>()
.setInputData(inputData)
.build()
WorkManager.getInstance(context).enqueue(request)
△ 将 RemoteWork 对象放入队列示例代码
组件名称是软件包名和类名的组合,您需要将其添加到工作请求的输入数据中,然后用这个输入数据创建工作请求,这样一来 WorkManager 就知道要绑定哪项服务了。我们照常将工作放入队列中,当 WorkManager 准备执行这项工作时,它首先根据输入数据中定义的内容找到绑定的服务,并执行 doRemoteWork 方法。这样一来,所有复杂繁琐的跨进程通信的任务都交给 WorkManager 来处理了。
总结
WorkManager 是应对长执行时间工作的推荐方案,推荐您使用 WorkManager 实现请求和取消长时间运行的工作任务。通过本文了解到如何以及何时使用加急工作 API,如何编写可靠的高性能多进程应用。希望这篇文章对您有所帮助,下一篇文章将对新的后台任务检查器做出简单介绍,敬请关注!
Codelab: 使用 WorkManager 处理后台任务
https://developer.android.google.cn/codelabs/android-workmanagerCodelab: WorkManager 进阶知识
https://developer.android.google.cn/codelabs/android-adv-workmanagerWorkManager 示例代码
https://github.com/android/architecture-components-samples/tree/main/WorkManagerSample
推荐阅读