13.6--WorkManager

Android 的后台机制是一个很复杂的话题,连我自己也没能完全搞明白不同Android 系统版本之间后台的功能与API 又发生了哪些变化。在很早之前,Android 系统的后台功能是非常开放的,Service 的优先级也很高,仅次于Activity,那个时候可以在Service 中做很多事情。但由于后台功能太过于开放,每个应用都想无限地占用后台资源,导致手机的内存越来越紧张,耗电越来越快,也变得越来越卡。为了解决这些情况,基本上Android 系统每发布一个新版本,后台权限都会被进一步收紧。

我印象中与后台相关的API 变更大概有这些:从4.4 系统开始AlermManager 的触发时间由原来的精准变为不精准,5.0 系统中加入了JobScheduler 来处理后台任务,6.0系统中引入了Doze 和App Standby 模式用于降低手机被后台唤醒的频率,从8.0 系统开始直接禁用了Service 的后台功能,只允许使用前台Service 。当然,还有许许多多小细节的修改,我没能全部列举出来。

这么频繁的功能和API 变更,让开发者就很难受了,到底该如何编写后台代码才能保证应用程序在不同系统版本上的兼容性呢?为了解决这个问题,Google 推出了WorkManager 组件。WorkManager 很适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlermManager 实现还是 JobScheduler 实现,从而降低了我们的使用成本。另外,它还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。

不过,我们还得先明确一件事情:WorkManager 和 Service 并不相同,也没有直接的联系。Service 是Android 系统的四大组件之一,它没有被销毁的情况下是一直保持在后台运行的。而WorkManager 只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然会得到执行,因此WorkManager 很适合用于执行一些定期和服务器进行交互的任务,不如周期性地同步数据,等等。

另外,使用WorkManager 注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减少CPU 被唤醒的次数,从而有效延迟电池的使用时间。

那么下面我们就开始学习WorkManager 的具体用法。

 

13.6.1 WorkManager 的基本用法

要想使用WorkManager ,需要先在app/build.gradle 文件中添加如下的依赖:

dependencies {
    ...
    implementation 'androidx.work:work-runtime:2.2.0'
   
}

将依赖添加完成之后,我们就把装备工作做好了。

WorkManager 的基本用法其实非常简单,主要分为以下3步:

(1)定义一个后台任务,并实现具体的任务逻辑;

(2)配置该后台任务的运行条件和约束信息,并构建后台任务请求;

(3)将该后台任务请求传入WorkManager 的enqueue() 方法中,系统会在何时的时间运行。

那么接下来我们就按照上述步骤一步步进行实现。

第一步要定义一个后台任务,这里创建一个SimpleWorker 类,代码如下所示:

class SimpleWorker(context: Context,params:WorkerParameters) :Worker(context,params){
    override fun doWork(): Result {
        Log.d("SimpleWorker", "do work in SimpleWorker ")
        return Result.success()
    }
}

后台任务的写法非常固定,也很好理解。首先每一个后台任务都必须继承自Worker 类,并调用它唯一的构造函数。然后重新父类中的doWork() 方法,在这个方法中编写具体的后台任务逻辑即可。

doWork() 方法不会运行在主线程中,因此你可以放心地在这里执行耗时逻辑,不过这里简单起只是打印了一行日志。另外,doWork() 方法要求返回一个Result 对象,用于表示任务的运行结果,成功就返回Result.success() ,失败就返回Result.failuer() 。除此之外,还有一个Result.retry() 方法,它其实也代表着失败,只是可以结合WorkRequest.Builder 的setBackoffCriteria() 方法来重新执行任务,我们稍后会进行学习。

没错就是这么简单,这样一个后台任务就定义好了。接下来可以进入第二步,配置该后台任务的运行条件和约束信息。

这一步其实也是最复杂的一部,因为可配置的内容非常多,不过目前我们还只是学习WorkManager 的基本用法,因此只进行最基本的配置就可以了,代码如下所示:

val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()

可以看到,只需要把刚才创建后台任务所对应的Class 对象传入OneTimeWorkRequest.Builder 的构造函数中,然后调用build() 方法即可完成构建。

OneTimeWorkRequest.Builder 是 WorkRequest.Builder 的子类,用于构建单次运行的后台任务请求。WorkRequest.Builder 还有另外一个子类 PeriodicWorkRequest.Builder ,可用于构建周期性运行的后台任务请求,但是为了降低设备性能消耗, PeriodicWorkRequest.Builder 构建函数中传入的运行周期间隔不能短于 15 分钟,示例代码如下:

val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java,15,TimeUnit.MINUTES).build()

最后一步,将构建出的后台任务请求传入 WorkManager 的 enqueue() 方法中,系统就会在合适的时间去运行了。

        WorkManager.getInstance(this).enqueue(request)

整体的用法就是这样,现在我们来测试一下吧。首先在activity_main.xml 中新增一个“Do Work” 按钮,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    ...

    <Button
        android:id="@+id/doWorkBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Do Work"/>
</LinearLayout>

由于activity_main.xml 中的按钮已经比较多了,如果新增的按钮已经超出了你的手机屏幕,可以使用我们之前学习的ScrollView 空间来滚动查看屏幕外的内容。

接下来修改MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        
        doWorkBtn.setOnClickListener { 
            val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
            WorkManager.getInstance(this).enqueue(request)
        }

    }


    ...
}

代码非常简单,就是在“Do Work” 按钮的点击事件中构建后台任务请求,并将请求传入WorkManager 的 enqueue() 方法中。后台任务的具体运行时间是由我们所指定的约束以及系统自身的一些优化所决定的,由于这里没有指定任何约束,因此后台任务基本上会在点击按钮之后立刻运行。

现在重新运行一下程序,并点击“Do Work” 按钮,观察Logcat 中的打印日志,如图所示:

 

13.6.2 使用WorkManager 处理复杂的任务

在上一小节中,虽然我们成功运行了一个后台任务,但是由于不能控制它的具体运行时间,因此并没有什么太大的实际用处。当然,WorkManager 是不可能没有提供这样的接口的,事实上除了运行事件之外,WorkManager 还允许我们控制许多其他方面的东西,下面就来具体看一下吧。

首先从最简单的看起,让后台任务在指定的延迟时间后运行,只需要借助setInitiaDelay() 方法就可以了,代码如下所示:

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

这就表示我们希望让SimpleWorker 这个后台任务在5 分钟后运行。你可以自由选择时间的单位,毫秒、秒、分钟、小时、天都可以。

可以控制运行时间之后,我们再增加一些别的功能,比如说给后台任务请求添加标签:

            val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
                .setInitialDelay(5,TimeUnit.MINUTES)
                .addTag("simple")
                .build()

那么添加了标签有什么好处呢?最主要的一个功能就是我们可以通过标签来取消后台任务请求:

            WorkManager.getInstance(this).cancelAllWorkByTag("simple")

当然,即使没有标签,也可以用过id 来取消后台任务请求:

            WorkManager.getInstance(this).cancelWorkById(request.id)

但是,使用id 只能取消单个后台任务请求,而使用标签的话,则可以将统一标签的所有后台任务请求全部取消,这个功能在逻辑复杂的场景下尤其有用。

除此之外,我们也可以使用如下代码来一次性取消所有后台任务请求:

            WorkManager.getInstance(this).cancelAllWork()

另外,我们在上一小节中讲到,如果后台任务的doWork() 方法中返回了Result.retry(),那么是可以结合setBackoffCriteria() 方法来重新执行任务的,具体代码如下所示:

            val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
                .setInitialDelay(5,TimeUnit.MINUTES)
                .addTag("simple")
                .setBackoffCriteria(BackoffPolicy.LINEAR,10,TimeUnit.SECONDS)
                .build()

setBackoffCriteria() 方法接收3个参数:第二个和第三个参数用于指定在多久之后重新执行任务,最短时间不能少于10秒钟;第一个参数则用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟。这其实很好理解,假如任务一直执行失败,不断地重新执行似乎并没有什么意思,只会徒增设备的性能消耗。而随着失败次数的增多,下次重试的时间也应该进行适当的延迟,这才是更加合理的机制。第一个参数的可选值有两种,分别是LINEAR 和 EXPONENTIAL ,前者代表下次重试时间以线性的方式延迟,后者代表下次重试时间以指数的方式延迟。

了解了Result.retry() 的作用之后,你一定还想知道,doWork() 方法中返回Result.success() 和Result.failure() 又有什么作用?这两个返回值其实就是用于通知任务运行结果的,我们可以使用如下代码对后台任务的运行结果进行监听:

            WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id).observe(this){
                workInfo -> 
                if (workInfo.state == WorkInfo.State.SUCCEEDED){
                    Log.d("MainActivity", "do work succeeded")
                }else if (workInfo.state == WorkInfo.State.FAILED){
                    Log.d("MainActivity", "do work failed")
                }
            }

这里调用了 getWorkInfoByIdLiveData() 方法,并传入后台任务请求的id ,会返回一个LiveData 对象。然后我们就可以调用LiveData 对象的observer() 方法来观察数据变化了,以此监听后台任务的运行结果。

另外,你也可以调用getWorkInfosByTagLiveData() 方法,监听同一签名下所有后台任务请求的运行结果,用法是差不多的,这里就不再进行解释了。

接下来,我们再来看一下WorkManager 中比较用特色的一个功能 —— 链式服务。

假设治理定义了3 个独立的后台任务:同步数据、压缩数据和上传数据。现在我们想要实现先同步,再压缩、最后上传的功能,就可以借助链式任务来实现,代码示例如下:

            WorkManager.getInstance(this)
                .beginWith(sync)
                .then(compress)
                .then(upload)
                .enqueue()

这段代码还是比较好理解的,相信你一看就能懂。beginWith() 方法用于开启一个链式任务,至于后面要接上什么样的后台任务,只需要使用then() 方法来连接即可。另外 WorkManager 还要求,必须在前一个后台任务运行成功之后,下一个后台任务才会运行。也就是说,如果某个后台任务运行失败,或者被取消了,那么接下来的后台任务就都得不到运行了

在本节的最后,我还想多说几句。前面所介绍的WorkManager 的所有功能,在国产手机上都有可能得到不正确的运行。这是因为绝大多数的国产手机厂商在进行Android 系统定制的时候会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序。而被杀死的应用程序既无法接收广播,也无法运行WorkManager 的后台任务。这个功能虽然与 Android 原生系统的设计理念并不相符,但是我们也没有什么解决办法。或许就是因为有太多恶意应用总是想要无限占用后台,国家手机才增加了这个功能吧。因此这里给你的建议就是,WorkManager 可以用,但是千万别依赖它去实现什么核心的功能,因为它在国产手机上可能会非常不稳定。

好了,关于WorkManager ,你所需要知道的内容大概就是这些了,那么我们本章对于Jetpack 的学习也就到此为止。目前你已经具备了开发一款高质量架构Android 应用的能力,在第15 章中会给你真正的实战机会。但是现在,我们还是按照惯例,进入本章的Kotlin 课堂,学习更多的知识和技能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值