Android Jetpack 架构组件之 WorkManger

一、前言

对于后台任务,我们先回顾以前的做法:以前我们在处理后台任务时,一般都是使用Service(含IntentService)或者线程/线程池,而Service不受页面生命周期影响,可以常驻后台,很适合做一些定时、延时任务,或者其他一些肉眼不可见的神秘勾当。 在处理一些复杂需求时,比如监听网络环境自动暂停重启后台上传下载这类变态任务,我们需要用Service结合Broadcast一起来做,非常的麻烦,再加上传输进度的回调,让人想疯!

同时大量的后台任务过度消耗了设备的电量,比如多种第三方推送的service都在后台常驻,不良App后台自动上传用户隐私也带来了隐私安全问题。

1、6.0 (API 级 23) 引入了Doze机制和应用程序待机。当屏幕关闭且设备静止时, 打盹模式会限制应用程序的行为。应用程序待机将未使用的应用程序置于限制其网络访问、作业和同步的特殊状态。
2、Android 7.0 (API 级 24) 有限的隐性广播和Doze-on-the-go.
3、Android 8.0 (API 级 26) 进一步限制了后台行为, 例如在后台获取位置并释放缓存的 wakelocks。

尤其在Android O(8.0)中,谷歌对于后台的限制几乎可以称之为变态:
Android 8.0有一项复杂功能:系统不允许后台应用创建后台服务。因此Android 8.0引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground()方法以显示新服务的用户可见通知。如果应用在此时间限制内未调用startForeground(),则系统将停止服务并声明此应用为ANR。

而且加入了对静态广播的限制
Android 8.0 让这些限制更为严格。针对 Android 8.0的应用无法继续在其清单中为隐式广播注册广播接收器。隐式广播是一种不专门针对该应用的广播。例如,ACTION_PACKAGE_REPLACED就是一种隐式广播,因为它将发送到注册的所有侦听器,让后者知道设备上的某些软件包已被替换。不过,ACTION_MY_PACKAGE_REPLACED不是隐式广播,因为不管已为该广播注册侦听器的其他应用有多少,它都会只发送到软件包已被替换的应用。应用可以继续在它们的清单中注册显式广播。应用可以在运行时使用Context.registerReceiver()为任意广播(不管是隐式还是显式)注册接收器。需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的所有应用。在许多情况下,之前注册隐式广播的应用使用 JobScheduler作业可以获得类似的功能。

于此同时,官方推荐用5.0推出的JobScheduler替换Service + Broadcast的方案。并且在Android O,后台Service启动后的5秒内,如果不转为前台Service就会ANR!

于是乎,Google推出了全新的解决方案:WorkManager

WorkManager的出现,则是为应用程序中那些不需要及时完成的任务,提供统一的解决方案,以便在设备电量和用户体验之间达到一个比较好的平衡。

WorkManager最低兼容API 14,能依据设备的情况,选择不同的执行方案。在API Level 23+,通过JobScheduler来完成任务,而在API 23以下的设备中,通过AlarmManagerBroadcastReceivers组合完成任务。但无论采用哪种方案,任务最终都是交由Executor来完成。核心就在于什么时候调用Executor来完成。
也就是说,WorkManager可以自动维护后台任务,同时可适应不同的条件,同时满足后台Service和静态广播,内部维护着JobScheduler,而在6.0以下系统版本则可自动切换为AlarmManager+BroadcastReceivers组合。

二、WorkManager特点

1、针对不需要及时完成的任务
比如,发送应用程序日志,同步应用程序数据,备份用户数据等。站在业务的角度,这些任务都不需要立即完成,如果我们自己来管理这些任务,逻辑可能会非常复杂,若API使用不恰当,可能会消耗大量电量。

2、保证任务一定会被执行
WorkManager能保证任务一定会被执行,即使你的应用程序当前不在运行中,哪怕你的设备重启,任务仍然会在适当的时候被执行。这是因为WorkManager有自己的数据库,关于任务的所有信息和数据都保存在这个数据库中,因此,只要你的任务交给了WorkManager,哪怕你的应用程序彻底退出,或者设备重新启动,WorkManager依然能够保证完成你交给的任务

注意:
WorkManager不是一种新的工作线程,它的出现不是为了替代其它类型的工作线程。工作线程通常立即运行,并在执行完成后给到用户反馈。而WorkManager不是即时的,它不能保证任务能立即得到执行。
在这里插入图片描述

三、WorkManager的使用

相关类和概念
1、Worker: 定义需要执行的任务,继承此类并在此定义任务。

2、WorkRequest: 代表一个单独的任务。WorkRequest对象指定执行任务的Worker类。还可以给WorkRequest添加更多的细节,指定任务的执行环境。每一个WorkRequest都有一个自动生成的唯一ID,使用该ID可以用来获取任务的执行状态或者取消任务WorkRequest是一个抽象类,我们只能使用其两个子类 OneTimeWorkRequest (单次任务)或者 PeriodicWorkRequest (周期性任务)。

3、WorkManager:将WorkRequest传给WorkManager来入队任务。当条件合适时将执行任务。

4、WorkInfo: 通过WorkInfo可以获取特定任务的执行状态,并在任务完成后获取其返回值。

WorkManager的使用,大致可分为以下几步:
A、 构建Work;
B、 配置WorkRequest;
C、 添加到WorkContinuation中;
D、 获取响应结果;

1、构建Work

A、添加依赖:
implementation "androidx.work:work-runtime: 2.4.0"
B、自定义Woker类,继承Worker类,复写doWork()方法,所有需要在任务中执行的代码都在该方法中编写。

WorkManager每一个任务都是由Work构成,所以Work是任务具体执行的核心所在。既然是核心所在,你可能会认为它会非常难实现,但恰恰相反,它的实现非常简单,你只需实现它的doWork方法即可。

public class CustomWoker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    @SuppressLint("RestrictedApi")
    override fun doWork(): Result {
        val resId = inputData.getString("input_data")
        val icon = inputData.getString("ICON");
        Logs.log("CustomWoker接收到的数据:resId=="+ resId + ",icon==" + icon)

        Thread.sleep(3 * 1000L)
        Logs.log("休眠(耗时操作)之后,执行了一波操作,")
        //TODO:任务在这里执行
        val data = Data.Builder().put("output_data", "work is success")
            .put("deal", "CustomWoker").build();
        return Result.success(data)
    }
}

所有代码的逻辑都在doWork中执行。
以上是逻辑代码,关键点是返回值Result.success(),它是一个Result类型,可用值有三个:
Result.success(): 任务执行成功。
Result.failure(): 任务执行失败
Result.retry(): 任务需要重新执行,如果出现这个返回结果,就需要与WorkRequest.Builder中的**setBackoffCriteria()**函数一起使用。

2、配置WorkRequest

使用WorkRequest配置任务。通过WorkRequest配置我们的任务何时运行以及如何运行。涉及到一下三个方面:

A、Constraints

Constraints的作用是约束任务的执行环境,控制任务在指定条件下运行,如足够内存、足够磁盘、足够电量、某种网络环境务等。不是必配对象,按需配置

var mConstraints = Constraints.Builder()
    .setRequiresDeviceIdle(true)                  //要求设备处于空闲状态
    .setRequiresCharging(true)                    //要求设备正在充电
    .setRequiresBatteryNotLow(true)               //要求设备未处于低电量状态
    .setRequiresStorageNotLow(true)               //要求设备未处于低内存状态
    .setRequiredNetworkType(NetworkType.METERED)  //要求设备网络环境
    .build()

将该Constraints设置到WorkRequest。WorkRequest是一个抽象类,它有两种实现:OneTimeWorkRequest和PeriodicWorkRequest,分别对应的是一次性任务周期性任务

B、OneTimeWorkRequest一次性任务

首先OneTimeWorkRequest是作用于一次性任务,即任务只执行一次,一旦执行完就自动结束。它的构建也非常简单:

val oneWorkReq: OneTimeWorkRequest = OneTimeWorkRequest.Builder(CustomWoker::class.java)
    .setConstraints(mConstraints) //设置触发条件
    .setInitialDelay(10, TimeUnit.SECONDS)//符合触发条件后,延迟10秒执行
    .addTag("SMS")
    .setInputData(inputData)// 传入inputData等参数
    .build()

需要注意的是:OneTimeWorkRequest任务不能够设置setBackoffCriteria(),否则抛异常:Cannot set backoff criteria on an idle mode jobPeriodicWorkRequest任务则无妨。

setInitialDelay():设置延迟执行任务。假设你没有设置触发条件,或者当你设置的触发条件符合系统的执行要求时,系统有可能立刻执行该任务,但如果你希望能够延迟执行,那么可以通过**setInitialDelay()**方法,延后任务的执行。

setBackoffCriteria():设置退避策略。假如Worker线程的执行出现了异常,比如服务器宕机,那么你可能希望过一段时间,重试该任务。那么你可以在Worker的doWork()方法中返回Result.retry(),系统会有默认的指数退避策略来帮你重试任务,你也可以通过**setBackoffCriteria()**方法,自定义指数退避策略。通常二者结合起来使用。

addTag():为任务设置Tag标签。设置Tag后,你就可以通过该标签跟踪任务的状态:
1、 WorkManager.getWorkInfosByTagLiveData(String tag):获取tag标签对应的任务的状态,任务的WorkInfo集合。
2、 WorkManager.cancelAllWorkByTag(String tag)取消tag标签对应的任务

在配置WorkRequest的过程中,我们还可以传入inputData与添加constraint约束条件等等。

val inputData: Data = Data.Builder().putString("input", "Hello World").put("ICON", R.mipmap.wjx).build()

上面的**“putString()”“put()”其实内部维护的是一组map集合,因此我们可以通过调用put*系列方法传递数据。
在doWork()方法中,通过InputData来获取上述oneWorkReq中传入的InputData数据:

override fun doWork(): Result {
    val resId = inputData.getString("input_data")    val icon = inputData.getInt("ICON", -1);
    ......    
	return Result.success()
}
C、PeriodicWorkRequest周期任务

OneTimeWorkRequestPeriodicWorkRequest,分别对应的是一次性任务和周期性任务。一次性任务,即任务在成功完成后,便彻底结束。而周期性任务则会按照设定的时间定期执行。二者使用起来没有太大差别。

需要注意的是
周期性任务的间隔时间不能小于15分钟(底层源码就已经控制死了,小于15分钟按15分钟算)。

periodicWork = PeriodicWorkRequest.Builder(CustomWoker::class.java, 15, TimeUnit.MINUTES)
    .addTag("SMS")
    .setConstraints(mConstraints)
    .setInitialDelay(10, TimeUnit.SECONDS)
    .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
    .setInputData(inputData)
    .build()

上面periodicWork的配置跟OneTimeWorkRequest的对象的配置完全一样。因此,就不在添加注释说明了。

3、将任务提交给系统。

**WorkManager.enqueue()**方法会将你配置好的WorkRequest交给系统来执行。

WorkManager.getInstance(this).enqueue(oneWorkReq)// 一次性任务的调用 
WorkManager.getInstance(this).enqueue(periodicWork)// 周期性任务的调用

4、观察任务的状态。

任务在提交给系统后,通过WorkInfo获知任务的状态,WorkInfo包含了任务的id、tag以及Worker对象传递过来的outputData以及任务当前的状态。有三种方式可以得到WorkInfo对象:

//按tag标签来观察任务状态
val workTag:ListenableFuture<List<WorkInfo>>=WorkManager.getInstance(this).getWorkInfosByTag("SMS")
//按任务的id标签来观察任务状态
val workId:ListenableFuture<WorkInfo> = WorkManager.getInstance(this).getWorkInfoById(oneWorkReq.id)
//通过给任务指定的唯一名来观察任务状态
val workUn:ListenableFuture<List<WorkInfo>> = WorkManager.getInstance(this).getWorkInfosForUniqueWork()
如果你希望能够实时获知任务的状态。这三个方法还有对应的LiveData方法。
//按tag标签来观察任务状态
val workByTag:LiveData<List<WorkInfo>> = WorkManager.getInstance(this).getWorkInfosByTagLiveData("SMS")
//按任务的id标签来观察任务状态
val workById:LiveData<WorkInfo> = WorkManager.getInstance(this).getWorkInfoByIdLiveData(oneWorkReq.id)
val workByUn:LiveData<List<WorkInfo>>=WorkManager.getInstance(this).getWorkInfosForUniqueWorkLiveData()
WorkManager.getInstance(this).getWorkInfosByTagLiveData("SMS").observe(this, {//通过tag标签
    it.forEach {        
    	Logs.log("workInfo:" + it)    
    }
})
WorkManager.getInstance(this).getWorkInfoByIdLiveData(oneWorkReq.id).observe(this, {//通过id
    Logs.log("workInfo:" + it)
})

5、Worker 的各种状态说明

在Worker 生命周期内会经历不同的 State,相关信息在 WorkInfo 对象中获取,包括 Worker 的 id、tag、当前 State 和返回数据:
A、 如果有尚未完成的前提性工作,则工作处于BLOCKED状态。
B、 如果工作能够在满足约束条件和时机条件立即运行,则被视为处于ENQUEUED状态。
C、 当Worker在活跃地执行时,其处于RUNNING状态。
D、 如果 Worker返回Result.success(),则被视为处于SUCCEEDED状态,这是一种终止状态只有 OneTimeWorkRequest可以进入这种状态
E、 如果Worker返回Result.failure(),则被视为处于FAILED状态。这也是一个终止状态;只有OneTimeWorkRequest可以进入这种状态。所有依赖工作也会被标记为FAILED,并且不会运行。
F、 当取消尚未终止的WorkRequest,它会进入CANCELLED状态。所有依赖工作也会被标记为CANCELLED,并且不会运行

6、取消任务。

与观察任务类似的,我们也可以根据Id或者Tag取消某个任务,或者取消所有任务。
在这里插入图片描述

WorkManager.getInstance(this).cancelAllWorkByTag("SMS")               //取消标签Tag对应的任务
WorkManager.getInstance(this).cancelAllWork()                          //取消所有的任务
WorkManager.getInstance(this).cancelWorkById(oneWorkReq.id)           //取消id对应的任务
WorkManager.getInstance(this).cancelUniqueWork("uniqueWorkName ")  //取消uniqueWorkName名对应的任务

7、参数传递

WorkManager和Worker之间的参数传递,通过Data对象来完成。

WorkManager通过**setInputData()**方法向Worker传递数据:见《配置WorkRequest》章节靠后的部分。

Worker中接收数据,并在任务执行完成后,向WorkManager返回数据:

override fun doWork(): Result {
......
    val resId = inputData.getString("input_data")    val icon = inputData.getInt("ICON", -1);
    val data = Data.Builder().put("output_data", "work is success").build();
    return Result.success(data)
}

WorkManager通过LiveDataWorkInfo.getOutputData(),得到从Worker传递过来的数据:

WorkManager.getInstance(this).getWorkInfoByIdLiveData(oneWorkReq.id).observe(this, {
    if (it != null && it.getState() === WorkInfo.State.SUCCEEDED) {
        val outputData: String? = it.outputData.getString("output_data")
        Logs.log("workInfo:" + outputData)
    }
})

需要注意的是:Data只能用于传递一些小的基本类型数据,且数据最大不能超过10kb!!!!!

8、WorkContinuation任务链

WorkContinuation任务链只能添加OneTimeWorkRequest任务,如果你有一系列的任务需要顺序执行,那么可以利用:

WorkManager.beginWith().then().then()...enqueue()
WorkManager
	.getInstance(this)
	.beginWith(oneWorkReq)
	.then(oneWorkReq2)
	.then(oneWorkReq3)
	.enqueue()

另外就是beginWith()then()函数还可以传入MutableList<OneTimeWorkRequest>类型的参数:

val list:MutableList<OneTimeWorkRequest> = mutableListOf(oneWorkReq, oneWorkReq2)
WorkManager.getInstance(this).beginWith(list).then(oneWorkReq3).enqueue()
WorkManager.getInstance(this).beginWith(oneWorkReq3).then(list).enqueue()
WorkManager.getInstance(this).beginWith(list).then(list).then(list).enqueue()

假设有更复杂的任务链(并行任务链),你还可以考虑使用**WorkContinuation.combine()**方法,将任务链组合起来

val work1: WorkContinuation = WorkManager.getInstance(this).beginWith(oneWorkReqA).then(oneWorkReqB)
val work2: WorkContinuation = WorkManager.getInstance(this).beginWith(oneWorkReqC).then(oneWorkReqD)
val taskList: MutableList<WorkContinuation> = ArrayList()
taskList.add(work1)		
taskList.add(work2)
WorkContinuation.combine(taskList).then(oneWorkReqE).enqueue()

另外,我们还可以通过beginUniqueWork创建一个唯一工作顺序的任务链

WorkManager
	.getInstance(this)
    .beginUniqueWork("UniWork", ExistingWorkPolicy.APPEND, list)
    .then(oneWorkReq)
    .enqueue()

beginUniqueWork()方法的三个参数:
A、String uniqueWorkName:给该唯一工作顺序指定一个名称name;

B、ExistingWorkPolicy existingWorkPolicy:是设置name相同时的表现,它三个值,分别为:
a)、 REPLACE: 当有相同name且未完成的链式请求时,将原来的进度取消并删除,重新加入新的链式请求;
b)、 KEEP: 当有相同name且未完成的链式请求时,链式请求保持不变,忽略新建的任务;
c)、 APPEND: 当有相同name且未完成的链式请求时,将新的链式请求追加到原来的子队列中,即当原来的链式请求全部执行后才开始执行。

C、 List<OneTimeWorkRequest> work:为OneTimeWorkRequest类型的任务

唯一执行顺序的任务可以用以处理出现某任务还没执行完成,又被重复提交的情况。

不管是beginWith还是beginUniqueWork,它都会返回WorkContinuation对象,通过该对象我们可以将后续任务加入到链式请求中:

val mWorkContinuation:WorkContinuation = WorkManager.getInstance(this).beginUniqueWork("Unique", ExistingWorkPolicy.APPEND, list).then(oneWorkReq)
mWorkContinuation.then(oneWorkReq).then(oneWorkReq2).enqueue()

通过beginUniqueWork我们可以继续通过**then()**函数往该任务链上面添加其他的任务,比如上面的例子中我们在后面又新增了两个工作任务。
在这里插入图片描述
PeriodicWorkRequest任务不支持建立链式请求,这一点需要注意。原因很简单:周期性的任务原则上是没有终止的,是个闭环,所以也就不存在所谓的链了。

9、部分方法解释

**setRequiredNetworkType (NetworkType requiredNetworkType):**指定任务执行时的网络状态。其中状态如下:

	NOT_REQUIRED:不需要网络;
	CONNECTED:任何可用网络;
	UNMETERED:需要不计量网络,如WiFi;
	NOT_ROAMING:需要非漫游网络;
	METERED:需要计量网络,如4G、5G等。
setRequiresBatteryNotLow (boolean requiresBatteryNotLow):设备电池电量低于阀值时是否启动任务,默认falsesetRequiresCharging (boolean requiresCharging):设备在充电时是否启动任务。
setRequiresDeviceIdle (boolean requiresDeviceIdle):设备是否为空闲时是否启动任务。
setRequiresStorageNotLow (boolean requiresStorageNotLow):设备储存空间低于阀值时是否启动任务。

四、最后

虽然WorkManager宣称,能够保证任务得到执行,但我在真实设备中,发现应用程序彻底退出与重启设备,任务都没有再次执行。查阅了相关资料,发现这应该与系统有关系。我们前面也提到了,WorkManager会根据系统的版本,决定采用JobScheduler或是AlarmManager+BroadcastReceivers来完成任务。但是这些API很可能会受到OEM系统的影响。比如某个系统不允许AlarmManager自动唤起,那么WorkManager很可能就无法正常使用。

而后我在Google原生系统的模拟器中进行测试,发现无论是彻底退出应用程序,或是重启设备,任务都能够被执行。所以,WorkManager在真实设备中不能正常使用,很可能就是系统的问题。因此,开发者在使用WorkManager作为解决方案时,一定要慎重。

另外,我还发现,周期任务的实际执行,与所设定的时间差别较大。执行时间看起来并没有太明显的规律。并且在任务执行完成后,WorkInfo并不会收到Success的通知。查阅了相关资料,发现Android认为SuccessFailure都属于终止类的通知。意思是,如果发出这类通知,则表明任务彻底结束,而周期任务不会彻底终止,会一直执行下去,所以我们在使用LiveData观察周期任务时,不会收到Success这类的通知。这也是我们需要注意的地方。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值