WorkManager是一个兼容性强、灵活和简单的延迟后台任务框架。本文将从以下几个方面对 WorkManager 进行介绍:
- 先明确什么是 Android 中的后台任务?
- 为什么要使用 WorkManager?
- 通过一个简单的案例,介绍如何使用 WorkManager。
- 分析 WorkManager 的组成及原理。
- 最后对 WorkManager 的使用进行总结。
在分析 WorkManager 之前,先要明确后台任务是什么?
1. 什么是后台任务?要了解后台任务,先来看看 Android 是如何定义前台应用的。前台任务通常有以下几个特点:
- 具有一个处于前台的 Activity,Activity 的生命周期在resumed、 started 或 paused 状态;
- 具有一个前台的服务,即 ForegroundService;
- 一个前台的应用关联到该应用,这种关联方式可以是使用了该应用的ContentProvider,或者绑定了该应用的输入法服务 IME、壁纸服务 WallpaperService、NotificationListenerService、语音服务VoiceInteractionService、文本服务 Textservice 中的一个或者几个。
如果上面提到的这些都不满足,那么应用处于后台。
在后台运行应用会消耗有限的设备资源,比如内存和电池的电量,进而会影响用户体验。后台任务会缩短设备的续航时间,或者会造成设备的卡顿等。
于是 Google 在 Android 版本的演进过程中,对后台应用进行了各种限制,比如:
- 低耗电模式和应用待机模式,如果设备未插接电源,处于空闲状态一段时间且屏幕关闭,系统会进入低耗电或者待机模式,并对应用行为施加相应限制;
- 后台位置限制,对后台应用获取用户当前位置的频率进行限制;
- 后台服务限制,限制应用在后台运行服务,并禁止应用通过隐式调用 CPU 或网络资源等。
我们的应用不可能总处于前台,但是我们需要下载更新,需要与服务器进行同步,这些业务逻辑该怎么实现呢?Google 给我们提供了诸如 AsyncTask、AlarmManager、JobScheduler、SyncAdapter、Loader、Service、FirebaseJobDispatcher、GCM NetworkManager 等一系列框架。其中我们比较常用的有 AlarmManager、Service和 JobScheduler。
在 AlarmManager 文档中有这样一段提示:
Note: Beginning with API 19 (Build.VERSION_CODES.KITKAT) alarm delivery is inexact: the OS will shift alarms in order to minimize wakeups and battery use. There are new APIs to support applications which need strict delivery guarantees; see setWindow(int, long, long, android.app.PendingIntent) and setExact(int, long, android.app.PendingIntent).
可以看到在 API 19以后,使用 AlarmManager 不会像以前那样精确了。
再来看看 Service:
A Service is an application component that can perform long-running operations in the background, and it does not provide a user interface.
不恰当的使用 Service 和 AlarmManager,除了会对设备的续航时间有影响外,从 Android Oreo (API 26) 开始,如果一个应用的 targeting SDK 为 Android 8.0,那么当它在某些不被允许创建后台服务的场景下,调用 Service 的startService() 方法,会抛出IllegalStateException。
再来看看 JobScheduler:
JobScheduler 首先会调度一个任务,然后在合适的时机(比如说延迟若干时间之后,或者等手机空闲了)系统会开启你的MyJobService,然后执行 onStartJob() 里的处理逻辑。
ComponentName service = new ComponentName(this, MyJobService.class);
JobScheduler mJobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(jobId, serviceComponent)
.setRequiredNetworkType(jobInfoNetworkType)
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setExtras(extras).build();
mJobScheduler.schedule(jobInfo);
但是 JobScheduler 只能在 API 23 及其以上才能使用,在 API 23 以下可以用 JobDispatcher。而 JobDispatcher 是 Firebase 中的类,需要 Google Play Services 的支持。
综上,WorkManager 应运而生。
WorkManager 为后台任务提供了一套统一的解决方案,它可以根据 Android 电量优化以及设备的 API 等级,选择合适的方式执行。WorkManager 向后兼容到 API 14,并且对无论集成 Google Play Service 服务与否的设备都予以支持。使用 WorkManager 管理任务,允许任务延迟,并且保证任务能够执行到,即使应用关闭或设备重启。
3. 通过一个简单的案例,介绍如何使用 WorkManagerWorkManager 不适用于需要在特定时间触发的任务,也不适用立即任务。针对特定时间触发的任务使用 AlarmManager,立即执行的任务使用 ForegroundService。
如果我们要完成一个图片上传的任务,大致如下:
总结一下,包括以下内容:
- 在电量充足的情况下完成图片的过滤;
- 在存储空间允许的情况下完成图片的压缩;
- 最后在网络条件允许的条件下,完成图片的上传。
为此我们需要完成以下几个步骤:
**1. 首先在 Worker 中定义任务,即继承 Worker 类,实现 doWork()方法,确定输入输出的数据。**在这个案例中,输入和输出的数据都为图片的 uri,可以通过 Data 进行封装,值得注意的是,Data 对数据是有大小限制的,由参数 MAX_DATA_BYTES 限制,默认为 10KB。
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
try {
// Get the input
val imageUriInput = inputData.getString(Constants.KEY_IMAGE_URI)
// Do the work
val response = upload(imageUriInput)
// Create the output of the work
val imageResponse = response.body()
val imglink = imageResponse.data.link
// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val outputData = workDataOf(Constants.KEY_IMAGE_URI to imglink)
return Result.success(outputData)
} catch (e: Exception) {
return Result.failure()
}
}
fun upload(imageUri: String): Response {
TODO(“Webservice request code here”)
// Webservice request code here; note this would need to be run
// synchronously for reasons explained below.
}
}
**2. 然后使用 WorkRequest 创建请求。**WorkRequest 类的主要作用是配置任务的运行方式和时间。在创建 WorkRequest 对象时可以添加限制条件,指定输入,并且选择单次或者周期性的方式来执行任务。这里添加限制条件为在网络连接时。这里选择了单次任务 OneTimeWorkRequest,如果是周期性的任务可以选择 PeriodicWorkRequest。
// Create the Constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// Define the input
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
// Bring it all together by creating the WorkRequest; this also sets the back off criteria
val uploadWorkRequest = OneTimeWorkRequestBuilder()
.setInputData(imageData)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
setBackoffCriteria()方法可以设置在任务失败时的重试策略,可以设置重新调度需要等待的时间,配合着策略(框架给出了线性和指数两种方式 backoffPolicy)使用,默认开始30s,逐渐增加最多5小时。
3. 使用 WorkManager 的 enqueue()方法,将任务添加到队列中,
WorkManager.getInstance(context).enqueue(uploadWorkRequest)
至此完成了 WorkManager 上传图片的简单示例,即使应用退出或者设备重启,只要在满足网络的条件下,系统会在将来某个时刻完成上传图片的任务。
4. 分析 WorkManager 的组成及原理先来看看 WorkManager 由哪些类组成。
4.1 WorkManager 的组成按照惯例,先给出 WorkManager 相关的一张类图(基于work-runtime-ktx:2.2.0),在类图中根据职责的不同,进行了不同颜色的标注,绿色的 WorkManager 负责后台任务的调度;粉色的 Worker 及 WorkRequest 指代具体的后台任务,黄色的 Constrains 等主要是与后台任务,或者任务调度的参数配置及状态信息相关。下面逐一介绍:
-
Worker 类是一个抽象类,我们需要继承 Worker 类,并实现 doWork()方法,在 doWork()方法中实现我们需要在后台执行的业务逻辑,我们可以将业务封装在多个 Woker 的实现类中。
-
WorkRequest 代表一个单独的后台任务。WorkRequest 类也是一个抽象类,我们通常会直接使用它的两个子类,oneTimeWorkRequest 和 PeriodicWorkRequest。
-
WorkRequest 还有一个内部类 WorkRequest.Builder,OneTimeWorkRequest.Builder 和 PeriodicWorkRequest.Builder都是 WorkRequest.Builder 的子类,Builder 使用的是创建者模式,我们通过 Builder 实例来创建具体的 WorkRequest 对象。
-
WorkRequest 可以指定那个 Worker 来执行任务。每一个 WorkRequest 实例都会生成唯一的id,可以通过 id 取消任务,或者获取任务的状态信息等。
-
Constraints 类用来设定执行任务的条件,比如设备空闲时、充电时、电量充足时等等,通过 WorkRequest 的 setConstraints() 方法将 Constraints 对象传入。
-
WorkInfo 用来记录任务信息,WorkInfo 有一个内部枚举State,用来保存任务的状态,一个后台任务有以下六种状态、分别是 ENQUEUED、RUNNING、SUCCEEDED、FAILED、BLOCKED 及 CANCELLED。
-
WorkInfo 除了记录任务的状态信息,还记录着任务的 id,tag 等,如果后台任务有输出的数据,也通过 WorkInfo 的成员变量 Data 对象得到。
-
如果后台任务需要接收一些输入的参数数据,可以通过 Data 对象,Data.Builder 针对不同数据类型封装了一系列 putXXX()方法。后台任务执行后返回的结果,通过 Data 对象封装的一系列 getXXX()方法获取。
-
WorkContinuation,很多 WorkManager 的高级用法都与这个类有关,比如建立任务链等,后面在 WorkManager 的高级用法中详细介绍。
-
WorkManager,后台任务的调度器,它负责将我们创建好的 WorkRequest 对象添加到队列中,根据我们设定的约束条件(Constraints对象),并且会综合考虑设备资源的使用情况,进行任务的调度。
前面提到 WorkManager 保证任务能够执行到,即使应用关闭或设备重启。再补充一点,WorkManager 保证后台任务运行在**主线程之外的线程。**我们分析一下 WorkManager 是如何做到的。
先给出一张原理图:
我们逐一分析一下:
-
Internal TaskExecutor, 一个单线程的线程池,用来处理后台任务的入队,并将 WorkRequest 信息保存到数据库中。当 WorkRequest 的触发条件满足时,从数据库中取出,并告诉 WorkFactory 创建 Worker 实例。
-
WorkManager database,WorkManager 会将后台任务保存到本地的数据库中,这也说明了为什么应用退出,设备重启,WorkManager 都能保证后台任务会被执行。在数据库中会存储后台任务的状态信息,输入输出数据,运行时的触发条件等等。
-
WorkerFactory,用来创建具体的 Worker 实例。
-
Default Executor,WorkManager 默认将后台任务运行在 Default Executor 线程池中,Default Executor 会调用 Worker 实例的 doWork() 方法,这也说明了 WorkManager 默认的后台任务运行在非主线程。
我们再来看一下 WorkRequest 有哪些状态信息:
通常 WorkRequest 有以上 6 中状态信息,它们之间的转换关系:
-
ENQUEUED,当 WorkRequest 被添加到队列,并且处在任务链的下一个任务,等待触发条件被满足。
-
RUNNING,在这种状态时,任务正在被执行,即 Worker 中的 doWork() 方法正在执行。在这种状态时,可以通过调用 Result.retry()方法,任务会继续回到 ENQUEUED 状态。
-
SUCCEEDED,在这种状态时,任务已经执行完,对于周期性的任务,没有这一状态,而是直接进入 ENQUEUED 状态,等待触发条件满足,再次执行。
-
FAILED,在这种状态时,任务在失败的状态下,已经执行完,任务不会再被执行。
-
BLOCKED,WorkRequest 的触发条件不满足,并且也不是任务链中下一个要执行的任务。
-
CANCELLED,当任务被取消时,会处在该状态,被取消的任务不会被执行,任务处在 ENQUEUED、RUNNING、BLOCKED 时都可以被取消,进入 CANCELLED 状态。
分析完 WorkManager 的原理,再来看一下 WorkManager 的一些高级用法。
5.1 创建任务链在介绍 WorkManager 类图时,提到了 WorkContinuation 类,这个类主要用来将后台任务组装成任务链。需要指出的是,任务链只适合一次性任务 OneTimeWorkRequest,对于周期性任务 PeriodicWorkRequest 不适用;任务链中的某个任务的状态是 FAILURE,则整条任务链结束。
对于上面提到的上传图片的案例,如果现在要同时选择三张图片,进行压缩,上传。通过任务链的方式,可以用以下的方式:
WorkManager.getInstance()
.beginWith(Arrays.asList(
filterImageOneWorkRequest,
filterImageTwoWorkRequest,
filterImageThreeWorkRequest))
.then(compressWorkRequest)
.then(uploadWorkRequest)
.enqueue()
此外 WorkManager 允许用户可以创建任意的,非循环的任务依赖图,比如:
上面的任务链可以写成:
WorkContinuation left = workManager.beginWith(A).then(B);
WorkContinuation right = workManager.beginWith(C).then(D);
WorkContinuation final = WorkContinuation.combine(Arrays.asList(left, right)).then(E);
final.enqueue();
5.2 观察后台任务的状态
我们可以通过 WorkManager 中 getWorkInfoByXXX() 中的一系列方法,获取封装了 WorkInfo 的 LiveData 实例。WorkInfo 中封装了输出数据,及任务的状态信息。
比如上面的实例,如果图片上传成功后,将图片显示在 UI 上,可以进行如下操作,关于 LiveData 的分析可以参考之前的文章 Android 架构组件之 LiveData
// In your UI (activity, fragment, etc)
WorkManager.getInstance().getWorkInfoByIdLiveData(uploadWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
// Check if the current work's state is "successfully finished"
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
displayImage(workInfo.outputData.getString(KEY_IMAGE_URI))
}
})
6. 总结
WorkManager 作为 Android 推荐的后台管理工具,WorkManager 考虑了系统所有的后台限制,WorkManager 的任务可以单次执行、循环执行、组合执行,构成任务链,还可以对任务添加约束条件,比如设备空闲时,充电时,连接网络时等。
既然 WorkManager 这么强大,是不是所有的后台任务都可以使用 WorkManager 呢?答案是否定的。在选择后台管理工具之前,需要对应用的需求和场景的限制有清晰的认识。
如图中描述,WorkManager 不适用于需要在特定时间触发的任务,也不适用立即任务。
如果应用需要立刻执行一个由用户发起的任务,即使用户退出应用或关闭屏幕也不会影响任务的执行,使用前台服务。
如果需要在某一特定时间运行一个无法被推迟的任务,且该任务会触发操作并涉及用户交互,用 AlarmManager 中的 setExactAndAllowWhileIdle() 方法。常见的定时任务包括:服药提醒,电视节目开始前,向用户发送的提醒通知等。总结为下表。
| 用例 | 示例 | 选择方案 |
|---|---|---|
| 任务可推迟,必须要执行 | 1.上传日志信息;2.对上传/下载的数据进行加密解密。3.同步数据 | WorkManager |
| 用户发起的任务,需要立即执行,且应用退出后,任务依旧可以执行 | 1.音乐播放器;2.路线导航等 | ForegroundService |
| 特定时间,涉及与用户交互 | 1.闹钟;2.服药提醒;3.节目开始前的提醒通知等 | AlarmManager |
至此,WorkManager 就分析完了,下一篇,我们来分析 Android 架构组件的Paging,敬请期待。
更多内容,可以订阅 我的博客
参考链接
(译)从Service到WorkManager
WorkManager Basics
Create an input method
android.service.textservice



