Android开发学习笔记——Jetpack之WorkManager

WorkManager

概述

WorkManager是Jetpack中提供用于处理后台任务的一个库,使我们可以轻松调度那些即使在退出应用或重启设备时仍应运行的可延期异步任务。在实际的Android开发过程中,如果我们想要处理后台任务,我们可能会想到使用Service、AlarmManager等技术,实际上实现后台任务,Android系统提供了众多的相关API供我们使用,如下图:
在这里插入图片描述
那么我们应该如果合理选择相关技术实现呢?我们为什么需要使用WorkManager呢?对此,谷歌给出了如下解释:
在这里插入图片描述
上述英文,大致就是描述了对于Android用户而言,电池的电量消耗是他们关注的一个重点,而如何处理延迟后台任务需求对于App电量消耗有着很大的影响,因此谷歌提供了一个统一的后台任务延迟处理方法——WorkManager。而从2020.11.1开始,谷歌将开始围绕WorkManager来统一Android系统上的后台任务延迟处理需求。并且GCMNetworkManager将会被弃用。
那么WorkManager有什么优势呢?为什么要选择WorkManager呢?谷歌做出如下解释:
在这里插入图片描述
大致可以翻译如下:
WorkManager的API集成了FJD和GcmNetworkManager相关技术的特性,考虑到了延长电池寿命的问题,提供了一致的任务调度服务,并且向后兼容到API14。例如,如果你的app需要向服务器发送日志文件,此时等待设备充电并连接到WiFi再执行相关后台发送日志任务会更有效。在这种情况下,WorkManager将确保在满足给定的约束(充电和连接WiFi)时执行同步。此外,它不需要Google play服务。
除此之外,WorkManager还有以下优势:

  • 生命周期长,在app更新和设备重启后仍然能够持久化工作
  • 提供了一次性和周期性执行后台任务
  • 有效监控和关联后台任务
  • 可以将不同的后台任务链式一起执行

基本使用

首先,我们需要认识几个类,WorkManager的使用主要就依靠以下几个核心类:

  • Worker:后台任务的真正执行者,是一个抽象类,需要继承它实现doWork方法,实现要执行的后台任务;
  • WorkRequest:指定让哪个 Woker 执行任务,指定执行的环境,执行的顺序等。
    要使用它的子类 OneTimeWorkRequest 或 PeriodicWorkRequest;
  • WorkManager:管理任务请求和任务队列,单例对象,发起的 WorkRequest 会进入它的任务队列;
  • WorkStatus:包含有任务的状态和任务的信息,以 LiveData 的形式提供给观察者。

使用WorkManager主要包含以下几个步骤:

  1. 添加相关依赖
  2. 继承Work类,自定义Work类,编写相关后台任务实现
  3. 创建WorkRequest对象,指定相关环境约束,即后台任务的执行条件
  4. 将WorkRequest对象添加到WorkManager任务队列中

接下来,我们就从这几个步骤来学习下WorkManager的相关使用。
首先,我们添加相关依赖,如下:

def work_version = "2.4.0"
// WorkManager
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
// optional - RxJava2 support
implementation "androidx.work:work-rxjava2:$work_version"
// optional - GCMNetworkManager support
implementation "androidx.work:work-gcm:$work_version"
// optional - Test helpers
androidTestImplementation "androidx.work:work-testing:$work_version"

然后,我们继承Worker,实现doWork方法,我们可以在doWork中执行相关后台任务,在实际开发中,我们所需要执行的后台任务,就在doWork中实现,如上述的上传日志文件,就是在doWork方法中调用相关网络接口上传的。如下:

class TestWork(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams){

    override fun doWork(): Result {
        //执行后台任务
        Log.e("test", "执行后台任务")
        //返回任务执行状态
        return Result.success()
    }

}

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

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

Worker 定义工作单元,WorkRequest(及其子类)则定义工作运行方式和时间。定义了Worker后,我们需要创建一个WorkRequest对象,来执行后台任务请求。不过WorkRequest本身是一个抽象类,WorkManager为我们提供了其两个子类OneTimeWorkRequest和PeriodicWorkRequest。其中OneTimeWorkRequest表示任务只执行一次而PeriodicWorkRequest表示任务周期性执行。如下:

//创建WorkRequest对象
val request = OneTimeWorkRequest.from(TestWork::class.java)

最后,需要使用 enqueue() 方法将 WorkRequest 提交到 WorkManager,WorkManager 服务进行调度。如下:

//将WorkRequest对象加入到WorkManager调度队列中,等待执行
WorkManager.getInstance().enqueue(request)

然后,WorkManager就会执行Worker中的后台任务。运行如下:
在这里插入图片描述

但是,我们需要注意,实际上执行Worker的确切时间取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供最佳行为。

Worker的调度

在上述小节中,我们说过Worker 定义工作单元,WorkRequest(及其子类)则定义工作运行方式和时间。对于如何调度后台任务的执行,WorkManager 提供了很大的灵活性。而如何调度主要就是依靠WorkerRquest来设置的。WorkRequest 对象包含 WorkManager 调度和运行工作所需的所有信息。其中包括运行工作必须满足的约束、调度信息(例如延迟或重复间隔)、重试配置,并且可能包含输入数据(如果工作需要)。

调度一次性任务和定期任务

我们提到过,WorkRequest是一个抽象类,而WorkManager为我们提供了其两个子类,其中OneTimeWorkRequest表示任务只执行一次而PeriodicWorkRequest表示任务周期性执行。

调度一次性任务

OneTimeWorkRequest的使用相对简单,对于无需,其它配置的Worker,我们可以直接使用from方法创建对象,如下:

val request = OneTimeWorkRequest.from(TestWork::class.java)

而对于更加复杂的,可能需要配置更多调度信息的,我们可以使用其构造器,如设置输入数据,如下:

val request = OneTimeWorkRequest.Builder(TestWork::class.java)
            .setInputData(Data.Builder().build())
            .build()
调度周期性任务

如果我们所创建的后台任务需要多次执行,我们就需要使用到PeriodicWorkRequest,比如定期上传日志等需求。和OneTimeWorkRequest的使用没有什么区别,只是多了一个定期执行任务的参数,如下:

val request = PeriodicWorkRequest.Builder(TestWork::class.java, 15, TimeUnit.MINUTES)
   .setInputData(Data.Builder().build())
   .build()
//        val request = PeriodicWorkRequestBuilder<TestWork>(15, TimeUnit.MINUTES)
//            .setInputData(Data.Builder().build())
//            .build()

上述两种方法本质上没有区别,实际上PeriodicWorkRequestBuilder就是封装了一层而已。对于PeriodicWorkRequest的时间间隔参数,我们有两点需要注意:

  • 该时间间隔并不是实际上两次后台任务执行的时间间隔,而是两次重复任务执行的最短时间,因为WorkManager中实际任务执行的时间我们是无法确定的。根据约束条件的变化不同。如果约束条件未满足,那么即使超过时间间隔,也不会再次执行后台任务。
  • 可以定义的最短重复间隔是 15 分钟,这与WorkManager的内部实现相关。

环境约束

我们说过WorkManager可以在满足某种条件时再执行相关后台任务,这实际上就是环境约束。WorkManager 允许我们指定任务执行的环境,比如网络已连接、电量充足时等,可确保将工作延迟到满足最佳条件时运行。其中可指定环境约束类型如下图:
在这里插入图片描述
其中网络环境包括以下类型:
在这里插入图片描述
我们需要通过Constraints类来指定约束条件,然后通过setConstraints方法设置WorkRequest的约束,其中除了网络状态,其他设置项都是传入一个布尔值,具体如下:

//指定约束条件
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)  // 网络状态
    .setRequiresBatteryNotLow(true)                 // 不在电量不足时执行
    .setRequiresCharging(true)                      // 在充电时执行
    .setRequiresStorageNotLow(true)                 // 不在存储容量不足时执行
    .setRequiresDeviceIdle(true)                    // 在待机状态下执行,需要 API 23
    .build()
//创建WorkRequest对象
val request = PeriodicWorkRequest.Builder(TestWork::class.java, 15, TimeUnit.MINUTES)
    .setConstraints(constraints)
    .build()

当设置了约束条件后,只有约束条件满足后后台任务才会被执行,如果指定了多个约束,任务将仅在满足所有约束时才会运行。如果在任务运行时不再满足某个约束,WorkManager 将停止后台任务的执行并将在满足所有约束后重试。

延时任务

我们之前的简单测试demo发现,后台任务再运行后立即执行了,这是因为我们并没有设置任何约束条件,如果我们设置了约束条件,那么WorkManager只有再条件满足后才会执行。如果任务没有约束,或者当任务加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该任务。如果不希望任务立即运行,可以将任务指定为在经过一段最短初始延迟时间后再启动。
那么如果我们只是单纯的想要延迟一段时间后再执行或者满足约束条件后一段时间再执行呢?此时,我们就可以通过setInitialDelay来设置延时时长,如下:

val request = OneTimeWorkRequest.Builder(TestWork::class.java)
            .setInitialDelay(10, TimeUnit.MINUTES)
            .build()

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

WorkRequest的标记和获取

每个工作请求都有一个唯一标识符,该标识符可用于在以后标识该WorkRequest,以便取消后台任务或观察其进度。我们可以通过addTag来为WorkRequest设置标记,然后通过WorkManager.getInstance(context).getWorkInfosByTag获取一个 WorkInfo 对象列表,该列表可用于确定当前工作状态或者通过WorkManager.getInstance(context).cancelAllWorkByTag(String) 会取消带有特定标记的所有工作请求。如下:

 val request = OneTimeWorkRequest.Builder(TestWork::class.java)
            .addTag("test_tag")
            .build()
        val workInfo = WorkManager.getInstance(this).getWorkInfosByTag("test_tag")

任务链

在某些情况下,我们可能存在多个后台任务需要执行。此时,我们可以使用 WorkManager 创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序。当我们需要以特定顺序运行多个任务时,此功能尤其有用。对于串行执行的多个任务,我们可以使用beginWith和then方法来实现,如下:

WorkManager.getInstance(this)
            .beginWith(request1)
            .then(request2)
            .then(request3)
            .enqueue()

此时,request1、2、3在满足约束条件的情况下,会依次执行,如果有任务返回 Result.failure(),整个序列结束。而且beginWith和then方法都能够传入多个request,如下:

//request1、2先并行执行,然后再执行3
WorkManager.getInstance(this)
    .beginWith(listOf(request1, request2))
    .then(request3)
    .enqueue()

//request1执行,然后再并行执行2、3
WorkManager.getInstance(this)
     .beginWith(request1)
     .then(listOf(request2, request3))
     .enqueue()

有时可能会存在多个链并行执行的情况,如下图:
在这里插入图片描述
此时,我们需要使用 WorkContinuation.combine(List<OneTimeWorkRequest>) 方法联接多个任务链来创建更为复杂的序列。如下:

val chain1 = WorkManager.getInstance(this)
    .beginWith(workA)
    .then(workB)
val chain2 = WorkManager.getInstance(this)
    .beginWith(workC)
    .then(workD)
val chain3 = WorkContinuation
    .combine(listOf(chain1, chain2))
    .then(workE)
chain3.enqueue()

后台任务的管理和监听

在实际开发中,我们很可能需要对后台任务的执行进行管理和监听,此时,我们一般需要通过WorkManager以id或者是tag来获取对应的WorkInfo然后在进行处理。

监听后台任务的执行

对于后台任务的监听,我们需要先获取到对应任务的workInfo,具体存在如下方法:

//通过id获取workInfo
WorkManager.getInstance(this).getWorkInfoById(request.id)
//通过tag获取所有的workInfo
WorkManager.getInstance(this).getWorkInfosByTag("tag")
//通过唯一任务name获取workInfo
WorkManager.getInstance(this).getWorkInfosForUniqueWork("unique_name")

获取到了WorkInfo,我们就可以对其进行观察和管理了。同时,这些方法都存在对应的LiveData方法,我们可以获取workInfo的LiveData对象,从而监听到其状态的变化。

后台任务状态

在实际开发过程中,我们经常会需要通过后台任务的各种状态来执行相应代码,此时我们可以通过对workInfo的观察,来监听其state变化,如下:

WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id)
            .observe(this, Observer{
                val state = it.state
                Log.e("test_work","state :$state")
            })

运行结果如下:
在这里插入图片描述
我们可以看到,通过观察workInfo,我们监听到了后台任务的状态变化。

任务状态

对于一次性任务,工作的初始状态为 ENQUEUED。在 ENQUEUED 状态下,您的工作会在满足其 Constraints 和初始延迟计时要求后立即运行。接下来,该工作会转为 RUNNING 状态,然后可能会根据工作的结果转为 SUCCEEDED、FAILED 状态;或者,如果结果是 retry,它可能会回到 ENQUEUED 状态。在此过程中,随时都可以取消工作,取消后工作将进入 CANCELLED 状态。如下图:
在这里插入图片描述
SUCCEEDED、FAILED 和 CANCELLED 均表示此工作的终止状态。如果您的工作处于上述任何状态,WorkInfo.State.isFinished() 都将返回 true。

而对于周期性任务,只有一个终止状态 CANCELLED。这是因为定期工作永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。如下图:
在这里插入图片描述
还有一种我们尚未提到的最终状态,那就是 BLOCKED。此状态适用于一系列已编排的工作,或者说工作链。链接工作中介绍了工作链及其状态图。

后台任务执行进度

如果我们需要监听后台任务的执行进度,我们需要使用到CoroutineWorker类,这个Worker的一个子类,我们可以将自己的后台任务继承自CoroutineWorker,然后使用setProgress来更新进度,不过进度也是以Data来设置的,如下:

class TestWork(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams){

    override suspend fun doWork(): Result {
        for (i in 1..10){
        	//设置进度
            setProgress(workDataOf("progress" to i))
            delay(1000)
        }
        return Result.success()
    }

}

如果,我们需要监听进度变化,也是通过观察workInfo来进行监听的,如下:

WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id)
     .observe(this, Observer{
         if (it.state == WorkInfo.State.RUNNING){
             val progress = it.progress
             Log.e("test_work","progress :${progress.getInt("progress", 0)}")
         }
     })

运行结果如下:
在这里插入图片描述

后台停止和取消任务

如果您不再需要运行先前加入队列的工作,则可以要求将其取消。您可以按工作的 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() 的调用。如需执行任何清理操作,请替换此方法。如需了解详情,请参阅停止正在运行的工作器。

数据的输入和输出

在后台任务执行的时候,我们可能需要传入一些参数数据,并且在任务执行完毕的时候返回一些数据。此时,我们可以通过Data.Builder 创建的 Data 对象来传递数据,Data类似于Bundle,也是通过键值对来传递对象的。
如果我们需要向任务传递参数,先调用 WorkRequest.Builder.setInputData(Data) 方法,然后再创建 WorkRequest 对象。该方法将接受您使用 Data.Builder 创建的 Data 对象。Worker 类可以通过调用 Worker.getInputData() 来访问这些参数。如需输出返回值,任务应当将其包含在 Result 中,例如返回 Result.success(Data)。您可以通过查看任务的 WorkInfo 获得输出内容。如下:

//输入数据
val data = Data.Builder()
    .putString("message", "this is a input message")
    .build()
//创建WorkRequest对象
val request = OneTimeWorkRequest.Builder(TestWork::class.java)
    .setInputData(data)
    .build()
.....


class TestWork(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams){

    override fun doWork(): Result {
        //获取输入数据
        val message = inputData.getString("message")
        //执行后台任务
        Log.e("test", "执行后台任务,输入数据为:$message")
        val outputData = Data.Builder()
            .putString("message", "this is a output message")
            .build()
        //返回任务执行状态,并返回数据
        return Result.success(outputData)
    }

}

如果我们需要获取到任务执行完毕后的数据,任务的 WorkInfo 中会提供返回值,如下:

WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id)
            .observe(this, Observer{
                //任务执行完毕
                if (it.state.isFinished){
                    val data = it.outputData.getString("message")
                    Log.e("test_work", "任务执行完成,输出数据为:$data")
                }
            })

运行结果如下:
在这里插入图片描述
如果我们链接多个任务,一个任务的输出可以作为任务链中下一个任务的输入。如果是简单链(即一个 OneTimeWorkRequest 后面跟着另一个 OneTimeWorkRequest),第一个任务通过调用 Result.success(Data) 返回其结果,下一个任务通过调用 getInputData() 提取该结果。如果是更复杂的链(例如有多个任务都将输出发送给同一个后续任务),我们可以在 OneTimeWorkRequest.Builder 上定义 InputMerger,以指定当多个不同任务返回具有相同键的输出时应执行什么操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值