jetpackworkmanager看这一篇就够了~

Hlq_ Hlq_     2022-11-30     337

关键词:


前言

最近有读者反馈,在我的新书​​《Android Jetpack 开发:原理解析与应用实战》​​中并没有提及到WorkManager,这是因为目前这个东西在国内并不是很好用。最近因为工作需要正好研究了下,也作为补充章节分享给读者。

什么是WorkManager

按照官方描述,WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。

任务类型

WorkManager任务类型分为立即运行、长期运行和延期执行,使用方式与周期关系如下所示:

立即

一次性

OneTimeWorkRequest 和 Worker。如需处理加急工作,请对 OneTimeWorkRequest 调用 setExpedited()。

长期运行

一次性或定期

任意 WorkRequest 或 Worker。在工作器中调用 setForeground() 来处理通知。

可延期

一次性或定期

PeriodicWorkRequest 和 Worker。

接下来来看具体的使用方法。

入门使用

添加依赖库

本文代码使用Kotlin编写,所以这里仅引入Kotlin相关的库即可,在build.gradle中添加代码如下所示:

def work_version = "2.7.1"
implementation "androidx.work:work-runtime-ktx:$work_version"

如果使用的是Java语言该如何引用呢?听我的,放弃吧~

定义工作Worker

这里我们以上传日志文件任务为例,新建UploadLogWorker类,继承自Worker,代码如下所示:

class UploadLogWorker(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams)

override fun doWork(): Result
Log.d("打印线程", Thread.currentThread().name)
return Result.success()

继承自Worker的类需要重写doWork方法,我们可以在这个方法中执行具体的任务,这里为了有演示结果打印出线程的名称。

Result用于返回任务的执行结果Result.success表示执行成功;Result.failure、Result.retry则分别表示执行失败和失败后尝试重试。

创建任务请求WorkRequest

这里我们创建一个一次性的执行任务,代码如下所示:

val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.build()

将任务提交系统

创建好任务之后,就可以将任务提交系统,执行请求,代码如下所示:

WorkManager.getInstance(this).enqueue(uploadLogWorkerRequset)

运行App,运行结果如下图所示。

Jetpack

为任务传递参数

许多时候我们在执行任务的时候是需要参数的,比如上传日志文件我们要知道日志文件的路径或者其他参数,我们怎么样将参数传递给Worker呢?

我们可以通过WorkRequest的setInputData方法来设置参数,代码如下所示:

val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setInputData(workDataOf("filePath" to "file://***", "fileName" to "log.txt"))
.build()

这里我们传递了文件路径filePath和文件名fileName,在Worker通过getInputData方法接受,比如我们在doWork中接受参数并打印。代码如下所示:

override suspend fun doWork(): Result 
val filePath = inputData.getString("filePath")
val fileName = inputData.getString("fileName")
Log.d("接受的参数", "$fileName:$filePath")
return Result.retry()

运行程序,打印如下图所示。

Jetpack

这样我们就完成了一个最简单的WorkManager使用案例。接着我们来进一步的探索。

执行加急工作你所需要知道的

从 WorkManager 2.7 开始,我们可以调用setExpedited方法来告诉系统,我这个任务是加急任务,请尽快执行。修改代码如下所示:

val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()

setExpedited方法中的OutOfQuotaPolicy参数有两个枚举值,含义如下所示。

枚举值

含义

RUN_AS_NON_EXPEDITED_WORK_REQUEST

当系统无法为任务加急处理时,任务变成常规任务

DROP_WORK_REQUEST

当系统无法为任务加急处理时,删除该任务

所以我们这里声明为RUN_AS_NON_EXPEDITED_WORK_REQUEST即可。再次运行程序。

OK,完美运行???

不过我的手机是Android 12的,为了确保没问题,我们必须在Android 11 或低版本上执行一次。没崩溃,但是任务却没执行,我们看到了错误日志如下图所示。

Jetpack

Emm.. 一堆乱七八糟的,关键信息在这句话

Expedited WorkRequests require a ListenableWorker to provide an implementation for `getForegroundInfoAsync()`

从官方我们获取到了这些信息:在 Android 12 之前,工作器中的 ​​getForegroundInfoAsync()​​​ 和 ​​getForegroundInfo()​​​ 方法可让 WorkManager 在您调用 ​​setExpedited()​​​ 时显示通知。如果您想要请求任务作为加急作业运行,则所有的 ListenableWorker 都必须实现 ​​getForegroundInfo​​ 方法。

如果未能实现对应的 getForegroundInfo​​ 方法,那么在旧版平台上调用 ​​setExpedited​​ 时,可能会导致运行时崩溃。

了解到了这些,那我们就来实现​​getForegroundInfo()​​方法,修改UploadLogWorker代码如下所示:

class UploadLogWorker(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams)

override fun doWork(): Result
Log.d("打印线程", Thread.currentThread().name)
setForegroundAsync(getForegroundInfo())
return Result.success()


@SuppressLint("RestrictedApi")
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo>
val future = SettableFuture.create<ForegroundInfo>()
future.set(getForegroundInfo())
return future



fun getForegroundInfo(): ForegroundInfo
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
val channel = NotificationChannel(
"1",
"hh",
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(channel)


val notification = NotificationCompat.Builder(applicationContext, "1")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle(applicationContext.getString(R.string.app_name))
.setContentText("我是一个上传日志的任务")
.build()
return ForegroundInfo(1337, notification)



再次在Android11 上运行程序,发现打印出了日志,并显示了一个任务通知,如下图所示。

Jetpack

这一点是在执行加急工作时所必须要注意的。

协程工作CoroutineWorker

1、将继承类修改为CoroutineWorker

2、实现getForegroundInfo方法,内容与上getForegroundInfo一致

定时任务PeriodicWorkRequest

在3.2中我们定义了一次性任务OneTimeWorkRequestBuilder,现在我们将上传日志的这个任务修改为定时任务,代码如下所示:

val uploadLogWorkerRequset: WorkRequest = PeriodicWorkRequestBuilder<UploadLogWorker>(15,TimeUnit.MINUTES)
.build()

这里指定了,定时任务的周期是15分钟一次,可以定义的最短重复间隔就是 15 分钟,这一点开发者在测试的时候需要注意,不能傻傻的等着...,这里我就傻傻的等了15分钟,确保定时任务是可以执行的。

工作约束、延迟执行和重试策略

工作约束

很多情况下,我们需要为任务添加工作约束,比如上传日志的任务肯定是在有网络的条件下进行的,当前支持的约束条件如下所示。

NetworkType

约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。

BatteryNotLow

如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。

RequiresCharging

如果设置为 true,那么工作只能在设备充电时运行。

DeviceIdle

如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能。

StorageNotLow

如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。

比如我们现在为一次性任务添加约束为在连接wifi的情况下执行,首先用Constraints构建一个约束实例可以将多个约束条件放在一起。代码如下所示:

val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()

这里设置为仅在充电的时候执行。接着为任务构建器添加约束。

val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setConstraints(constraints)
.build()

这样一来任务就会在仅充电的时候执行了。

延迟执行

延迟执行适用于一次性任务和定时任务,但应用在定时任务事时对第一次执行有效,为啥呢?因为是定时任务呀~

我们为一次性任务设置延迟时间为5秒钟,代码如下所示:

val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setConstraints(constraints)
.setInitialDelay(5,TimeUnit.SECONDS)
.build()

运行程序,可以看到5秒钟后,程序才打印了日志,这里就不演示了。

重试策略

在3.2中定义Work中我们提到了Result.retry可以让任务重试,我们也可以自定义任务的重试策略和退避政策,我们通过具体的例子来解释。

val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL, OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()

最短退避延迟时间设置为允许的最小值,即 10 秒。由于政策为 LINEAR,每次尝试重试时,重试间隔都会增加约 10 秒。例如,第一次运行以 Result.retry() 结束并在 10 秒后重试;然后,如果工作在后续尝试后继续返回 Result.retry(),那么接下来会在 20 秒、30 秒、40 秒后重试,以此类推。

打印日志如下图所示。

Jetpack

我们可以看到,第一次任务失败后延迟了10秒重新执行,第二次延迟了20秒,第三次延迟了40秒...

观察工作执行结果

在任务完成后,我可能需要进行更新UI或者业务逻辑操作。我们可以通过注册监听器来观察 WorkInfo 的变化,以根据ID查询WorkInfo状态为例,代码如下所示:

WorkManager.getInstance(this).getWorkInfoByIdLiveData(uploadLogWorkerRequset.id).observe(this)
if (it.state == WorkInfo.State.SUCCEEDED)
Toast.makeText(this,"任务执行成功,更新UI",Toast.LENGTH_LONG).show()
else
//任务失败或重试

除了getWorkInfoByIdLiveData之外还有根据tag、name等查询的转化方法,这里读者可自行查看API。

运行程序,结果如下图所示。

Jetpack

类似的我们还可以通过cancelWorkById等方法来取消任务的执行。这里不做演示了。此外还有一些其他的特性感兴趣的读者可以自行实践。

总结

特性及注意事项

  • 在早于 Android 12 的 API 版本中,加急工作都是由前台服务执行的,而从 Android 12 开始,它们将由加急作业 (expedited job) 实现。所以在第4小节中,默认Android12上并不会显示通知栏
  • WorkManager 只是一个处理定时任务的工具
  • WorkManager 最早兼容到 API 14(Android 4.0)
  • 使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系 统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减 少CPU被唤醒的次数,从而有效延长电池的使用时间
  • WorkManager官方虽然称它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行。但是在国产手机中是不可能的,因为系统自己做了改动。但是在国产机上测试(OPPO)退出后,再进来也会执行之前的任务。这个时候可能就会有重复的任务执行。

问题

  • 任务添加到队列后,未开始执行前,如果是在onDestory中调用取消任务的方法是不可行的,此种情况下下次进来时仍然会有重复任务开始执行。原因不明。(原生系统、国产机一样)

这里遇到了这样一个问题,在这个文章发布的时候,我还不知道该如何解决。希望知道的大佬指教指教~

如果你想学习Jetpack更多精彩内容,欢迎购买我的新书《Android Jetpack开发:原理解析与应用实战》

elasticsearch入门,看这一篇就够了(代码片段)

Elasticsearch入门,看这一篇就够了前言可视化工具kibanakibana的安装kibana配置kibana的启动Elasticsearch入门操作操作index创建index索引别名有什么用删除索引查询索引exist索引操作document插入document查询document删除document更新document使用... 查看详情

json入门看这一篇就够了

什么是JSONJSON:JavaScriptObjectNotation【JavaScript对象表示法】JSON是存储和交换文本信息的语法。类似XML。JSON采用完全独立于任何程序语言的文本格式,使JSON成为理想的数据交换语言S为什么需要JSON提到JSON,我们就应该和XML来进行对... 查看详情

json格式对象该怎么传?看这一篇就够了!

一、List<Map>一般会使用在SQL语句的返回结果上代码如下:@Query(value="selectuser_name,user_agefromsys_userwheredept_id=?,nativeQuery=true)List<Map>findUserInfoById(LongdeptId);根据部门的ID查 查看详情

java中的多线程你只要看这一篇就够了

 https://www.cnblogs.com/wxd0108/p/5479442.html  查看详情

handler看这一篇就够了(代码片段)

Handler使用首先来熟悉一下Handler的四种使用方式,如果比较熟悉可以直接跳过:通过sendMessage消息机制来发送sendEmptyMessage(int);//发送一个空的消息sendMessage(Message);//发送消息,消息中可以携带参数sendMessageAtTime(Message,long... 查看详情

javanio看这一篇就够了

原文链接:https://mp.weixin.qq.com/s/c9tkrokcDQR375kiwCeV9w?现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技术,譬如Tomcat,Jetty。学习和掌握NIO技术已经不是一个JAVA攻城狮的加分技能,而是一个必备技能。在前篇文章... 查看详情

@override看这一篇就够了

一、是什么一句话:它是表示重写的注解@Override注解是伪代码,用于表示被标注的方法是一个重写方法。不写也完全可以,但强烈建议写上! 二、为什么用既然不写@Override也可以重写父类的方法,那为什么非要“多此一... 查看详情

java集合,看这一篇就够了

Java集合是java.util下特别有用的工具类,大致分为2个系列:Collection接;口和Map接口派生的。按体系分,则分为四种1. List:有序重复的集合;2.Set:无序不重复的集合3.Map:有映射关系的集合(key-value)4.Queue:代表队列集合的实现(JDK5... 查看详情

最全排序算法及优化,看这一篇就够了(代码片段)

最全排序算法总结看这一篇就够了没有经过总结的知识是沉重的,无用的瞧一瞧~博健的LeetCode题解:Gitbook版本传送门博健的LeetCode题解:CSDN传送门有趣的CSS:Gitbook传送门前端进阶笔记:Gitbook传送门目录... 查看详情

关于反爬虫,看这一篇就够了

编者:本文来自携程酒店研发部研发经理崔广宇在第三期【携程技术微分享】上的分享,以下为整理的内容概要。墙裂建议点击视频回放,“现场”围观段子手攻城狮大崔,如何高智商&高情商地完美碾压爬虫。。。关... 查看详情

关于反爬虫,看这一篇就够了(转)

https://segmentfault.com/a/1190000005840672者:本文来自携程酒店研发部研发经理崔广宇在第三期【携程技术微分享】上的分享,以下为整理的内容概要。墙裂建议点击视频回放,“现场”围观段子手攻城狮大崔,如何高智商&高情商地... 查看详情

java集合看这一篇就够了

大家好,这里是《齐姐聊数据结构》系列之大集合。话不多说,直接上图:Java集合,也称作容器,主要是由两大接口(Interface)派生出来的:Collection和Map顾名思义,容器就是用来存放数据的。那么这两大接口的不同之处在于:Colle... 查看详情

了解“预训练-微调”,看这一篇就够了

预训练-微调方法指的是首先在大数据集上训练得到一个具有强泛化能力的模型(预训练模型),然后在下游任务上进行微调的过程。预训练-微调方法属于基于模型的迁移方法(Parameter/Model-basedTransferLearning)... 查看详情

了解“预训练-微调”,看这一篇就够了

预训练-微调方法指的是首先在大数据集上训练得到一个具有强泛化能力的模型(预训练模型),然后在下游任务上进行微调的过程。预训练-微调方法属于基于模型的迁移方法(Parameter/Model-basedTransferLearning)... 查看详情

java注解看这一篇就够了

注解1.概念注解:说明程序的。给计算机看的注释:用文字描述程序的。给程序员看的注解的定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次... 查看详情

java数组,看这一篇就够了

在Java泛型出现之前,只有数组可以用来存储指定类型的对象;在自动装箱机制出现之前,只有数组可以用来存储基本数据类型;也就是说,在泛型和自动装箱机制出现之前,数组在Java当中的分量举足轻重。况且数组还是一种效... 查看详情

css渐变背景看这一篇就够了(代码片段)

CSS渐变背景看这一篇就够了在我们自己设计网页的时候,为了好看美观,颜色可谓是最让人头疼的一部分。尤其是在配色上又找不到一些好看的网站。今天我就来记录一些好看的渐变式背景,和一些常用的颜色网站。... 查看详情

☀️javanio?看这一篇就够了!!☀️(代码片段)

文章目录一、NIO简介1.1NIO概述1.2NIO&IO分析1.2.1IO操作流程1.2.2面向流与面向缓冲区1.2.3阻塞与非阻塞1.2.4.同步与异步二、Buffer基本应用2.1Buffer概述2.2Buffer基本应用三、Channel基本应用3.1Channel概述3.2FileChannel基本应用3.3SocketChanel基... 查看详情