Android 架构组件之 WorkManager

1,689 阅读12分钟

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 或网络资源等。

2. 为什么要使用 WorkManager?

我们的应用不可能总处于前台,但是我们需要下载更新,需要与服务器进行同步,这些业务逻辑该怎么实现呢?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 管理任务,允许任务延迟,并且保证任务能够执行到,即使应用关闭或设备重启。

WorkManager 不适用于需要在特定时间触发的任务,也不适用立即任务。针对特定时间触发的任务使用 AlarmManager,立即执行的任务使用 ForegroundService。

3. 通过一个简单的案例,介绍如何使用 WorkManager

如果我们要完成一个图片上传的任务,大致如下:

总结一下,包括以下内容:

  • 在电量充足的情况下完成图片的过滤;
  • 在存储空间允许的情况下完成图片的压缩;
  • 最后在网络条件允许的条件下,完成图片的上传。

为此我们需要完成以下几个步骤:

**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<UploadWorker>()
        .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对象),并且会综合考虑设备资源的使用情况,进行任务的调度。

4.2 WorkManager 的原理。

前面提到 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 状态。

5. WorkManager 的高级用法

分析完 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