一、前言
对于后台任务,我们先回顾以前的做法:以前我们在处理后台任务时,一般都是使用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以下的设备中,通过AlarmManager和BroadcastReceivers组合完成任务。但无论采用哪种方案,任务最终都是交由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 job。PeriodicWorkRequest任务则无妨。
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周期任务
OneTimeWorkRequest和PeriodicWorkRequest,分别对应的是一次性任务和周期性任务。一次性任务,即任务在成功完成后,便彻底结束。而周期性任务则会按照设定的时间定期执行。二者使用起来没有太大差别。
需要注意的是:
周期性任务的间隔时间不能小于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通过LiveData的WorkInfo.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):设备电池电量低于阀值时是否启动任务,默认false。
setRequiresCharging (boolean requiresCharging):设备在充电时是否启动任务。
setRequiresDeviceIdle (boolean requiresDeviceIdle):设备是否为空闲时是否启动任务。
setRequiresStorageNotLow (boolean requiresStorageNotLow):设备储存空间低于阀值时是否启动任务。
四、最后
虽然WorkManager宣称,能够保证任务得到执行,但我在真实设备中,发现应用程序彻底退出与重启设备,任务都没有再次执行。查阅了相关资料,发现这应该与系统有关系。我们前面也提到了,WorkManager会根据系统的版本,决定采用JobScheduler或是AlarmManager+BroadcastReceivers来完成任务。但是这些API很可能会受到OEM系统的影响。比如某个系统不允许AlarmManager自动唤起,那么WorkManager很可能就无法正常使用。
而后我在Google原生系统的模拟器中进行测试,发现无论是彻底退出应用程序,或是重启设备,任务都能够被执行。所以,WorkManager在真实设备中不能正常使用,很可能就是系统的问题。因此,开发者在使用WorkManager作为解决方案时,一定要慎重。
另外,我还发现,周期任务的实际执行,与所设定的时间差别较大。执行时间看起来并没有太明显的规律。并且在任务执行完成后,WorkInfo并不会收到Success的通知。查阅了相关资料,发现Android认为Success和Failure都属于终止类的通知。意思是,如果发出这类通知,则表明任务彻底结束,而周期任务不会彻底终止,会一直执行下去,所以我们在使用LiveData观察周期任务时,不会收到Success这类的通知。这也是我们需要注意的地方。