Android8.0后时代的后台任务JetPack-WorkManager详解

本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布

WorkManager详解

1、回顾一下之前的作法

之前咱们在处理后台任务时,通常都是使用Service(含IntentService)或者线程/线程池,而Service不受页面生命周期影响,能够常驻后台,因此很适合作一些定时、延时任务,或者其余一些肉眼不可见的神秘勾当。 在处理一些复杂需求时,好比监听网络环境自动暂停重启后台上传下载这类变态任务,咱们须要用Service结合Broadcast一块儿来作,很是的麻烦,再加上传输进度的回调,让人想疯!javascript

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

2、谷歌开始专项整顿

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

尤为在Android O(8.0)中,谷歌对于后台的限制几乎能够称之为变态:html

Android 8.0 有一项复杂功能;系统不容许后台应用建立后台服务。 所以,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。 在系统建立服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。 若是应用在此时间限制内未调用 startForeground(),则系统将中止服务并声明此应用为 ANR。java

并且加入了对静态广播的限制:android

Android 8.0 让这些限制更为严格。 针对 Android 8.0 的应用没法继续在其清单中为隐式广播注册广播接收器。 隐式广播是一种不专门针对该应用的广播。 例如,ACTION_PACKAGE_REPLACED 就是一种隐式广播,由于它将发送到注册的全部侦听器,让后者知道设备上的某些软件包已被替换。 不过,ACTION_MY_PACKAGE_REPLACED 不是隐式广播,由于无论已为该广播注册侦听器的其余应用有多少,它都会只发送到软件包已被替换的应用。 应用能够继续在它们的清单中注册显式广播。 应用能够在运行时使用 Context.registerReceiver() 为任意广播(无论是隐式仍是显式)注册接收器。 须要签名权限的广播不受此限制所限,由于这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的全部应用。 在许多状况下,以前注册隐式广播的应用使用 JobScheduler 做业能够得到相似的功能。nginx

于此同时,官方推荐用5.0推出的JobScheduler替换Service + Broadcast的方案。数组

而且在Android O,后台Service启动后的5秒内,若是不转为前台Service就会ANR!缓存

3、官方的推荐(qiang zhi)作法

场景 推荐
需系统触发,没必要完成 ThreadPool + Broadcast
需系统触发,必须完成,可推迟 WorkManager
需系统触发,必须完成,当即 ForegroundService + Broadcast
不需系统触发,没必要完成 ThreadPool
不需系统触发,必须完成,可推迟 WorkManager
不需系统触发,必须完成,当即 ForegroundService

4、WorkManager的推出

WorkManager 是一个 Android 库, 它在工做的触发器 (如适当的网络状态和电池条件) 知足时, 优雅地运行可推迟的后台工做。WorkManager 尽量使用框架 JobScheduler , 以帮助优化电池寿命和批处理做业。在 Android 6.0 (API 级 23) 下面的设备上, 若是 WorkManager 已经包含了应用程序的依赖项, 则尝试使用Firebase JobDispatcher 。不然, WorkManager 返回到自定义 AlarmManager 实现, 以优雅地处理您的后台工做。安全

也就是说,WorkManager能够自动维护后台任务,同时可适应不一样的条件,同时知足后台Service和静态广播,内部维护着JobScheduler,而在6.0如下系统版本则可自动切换为AlarmManager,好神奇!ruby

5、WorkManager详解

1.引入

implementation "android.arch.work:work-runtime:1.0.0-alpha06" // use -ktx for Kotlin 

2.重要的类解析

2.1 Worker

Worker是一个抽象类,用来指定须要执行的具体任务。咱们须要继承Worker类,并实现它的doWork方法:

class MyWorker:Worker() { val tag = javaClass.simpleName override fun getExtras(): Extras { return Extras(...) //也能够把参数写死在这里 } override fun onStopped(cancelled: Boolean) { super.onStopped(cancelled) //当任务结束时会回调这里 ... } override fun doWork(): Result { Log.d(tag,"任务执行完毕!") return Worker.Result.SUCCESS } } 
向任务添加参数
  1. 在Request中传参:

    val data=Data.Builder() .putInt("A",1) .putString("B","2") .build() val request2 = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS) .setInputData(data) .build() 
  2. 在Worker中使用:

    class MyWorker:Worker() { val tag = javaClass.simpleName override fun doWork(): Result { val A = inputData.getInt("A",0) val B = inputData.getString("B") return Worker.Result.SUCCESS } } 

固然除了上述代码中的方法以外,咱们也能够重写父级的getExtras(),并在此方法中把参数写死再返回也是能够的。

这里WorkManager就有一个不是很人性的地方了,那就是WorkManager不支持序列化传值!这一点让我怎么说啊,intent和Bundle都支持序列化传值,为何恰恰这货就不行?那么若是传一个复杂对象还要先拆解吗?

任务的返回值

很相似很相似的,任务的返回值也很简单:

override fun doWork(): Result { val A = inputData.getInt("A",0) val B = inputData.getString("B") val data = Data.Builder() .putBoolean("C",true) .putFloat("D",0f) .build() outputData = data//返回值 return Worker.Result.SUCCESS } 

doWork要求最后返回一个Result,这个Result是一个枚举,它有几个固定的值:

  • FAILURE 任务失败。
  • RETRY 遇到暂时性失败,此时可以使用WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit)来重试。
  • SUCCESS 任务成功。

看到这里我就很奇怪,官方不推荐咱们使用枚举,可是本身却一直在用,什么意思?

2.2WorkRequest

也是一个抽象类,能够对Work进行包装,同时装裱上一系列的约束(Constraints),这些Constraints用来向系统指明什么条件下,或者何时开始执行任务。

WorkManager向咱们提供了WorkRequest的两个子类:

  • OneTimeWorkRequest 单次任务。
  • PeriodicWorkRequest 周期任务。
val request1 = PeriodicWorkRequestBuilder<MyWorker>(60,TimeUnit.SECONDS).build() val request2 = OneTimeWorkRequestBuilder<MyWorker>().build() 

从代码中能够看到,咱们应该使用不一样的构造器来建立对应的WorkRequest。

接下来咱们看看都有哪些约束:

  • public boolean requiresBatteryNotLow ():执行任务时电池电量不能偏低。
  • public boolean requiresCharging ():在设备充电时才能执行任务。
  • public boolean requiresDeviceIdle ():设备空闲时才能执行。
  • public boolean requiresStorageNotLow ():设备储存空间足够时才能执行。
addContentUriTrigger
@RequiresApi(24) public @NonNull Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants) 

指定是否在(Uri指定的)内容更新时执行本次任务(只能用于Api24及以上版本)。瞄了一眼源码发现了一个ContentUriTriggers,这什么东东?

public final class ContentUriTriggers implements Iterable<ContentUriTriggers.Trigger> { private final Set<Trigger> mTriggers = new HashSet<>(); ... public static final class Trigger { private final @NonNull Uri mUri; private final boolean mTriggerForDescendants; Trigger(@NonNull Uri uri, boolean triggerForDescendants) { mUri = uri; mTriggerForDescendants = triggerForDescendants; } 

特么惊呆了,竟然是个HashSet,而HashSet的核心是个HashMap啊,谷歌声明不建议用HashMap,固然也就不建议用HashSet,但是官方本身在背地里面干的这些勾当啊...

setRequiredNetworkType
public void setRequiredNetworkType (NetworkType requiredNetworkType) 

指定任务执行时的网络状态。其中状态见下表:

|枚举|状态| |-|-| |NOT_REQUIRED|不须要网络| |CONNECTED|任何可用网络| |UNMETERED|须要不计量网络,如WiFi| |NOT_ROAMING|须要非漫游网络| |METERED|须要计量网络,如4G|

setRequiresBatteryNotLow
public void setRequiresBatteryNotLow (boolean requiresBatteryNotLow) 

指定设备电池电量低于阀值时是否启动任务,默认false。

setRequiresCharging
public void setRequiresCharging (boolean requiresCharging) 

指定设备在充电时是否启动任务。

setRequiresDeviceIdle
public void setRequiresDeviceIdle (boolean requiresDeviceIdle) 

指明设备是否为空闲时是否启动任务。

setRequiresStorageNotLow
public void setRequiresStorageNotLow (boolean requiresStorageNotLow) 

指明设备储存空间低于阀值时是否启动任务。

给任务加约束:
val myConstraints = Constraints.Builder()
        .setRequiresDeviceIdle(true)//指定{@link WorkRequest}运行时设备是否为空闲 .setRequiresCharging(true)//指定要运行的{@link WorkRequest}是否应该插入设备 .setRequiredNetworkType(NetworkType.NOT_ROAMING) .setRequiresBatteryNotLow(true)//指定设备电池是否不该低于临界阈值 .setRequiresCharging(true)//网络状态 .setRequiresDeviceIdle(true)//指定{@link WorkRequest}运行时设备是否为空闲 .setRequiresStorageNotLow(true)//指定设备可用存储是否不该低于临界阈值 .addContentUriTrigger(myUri,false)//指定内容{@link android.net.Uri}时是否应该运行{@link WorkRequest}更新 .build() val request = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS) .setConstraints(myConstraints)//注意看这里!!! .build() 
给任务加标签分组
val request1 = OneTimeWorkRequestBuilder<MyWorker>()
                .addTag("A")//标签 .build() val request2 = OneTimeWorkRequestBuilder<MyWorker>() .addTag("A")//标签 .build() 

上述代码我给两个相同任务的request都加上了标签,使他们成为了一个组:A组。这样的好处是之后能够直接控制整个组就好了,组内的每一个成员都会受到影响。

2.3 WorkManager

通过上面的操做,相信咱们已经可以成功建立request了,接下来咱们就须要把任务放进任务队列,咱们使用WorkManager

WorkManager是个单例,它负责调度任务而且监放任务状态。

WorkManager.getInstance().enqueue(request) 

当咱们的request入列后,WorkManager会给它分配一个work ID,以后咱们可使用这个work id来取消或者中止任务:

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

注意:WorkManager并不必定能结束任务,由于任务有可能已经执行完毕了。

同时,WorkManager还提供了其余结束任务的方法:

  • cancelAllWork():取消全部任务。
  • cancelAllWorkByTag(tag:String):取消一组带有相同标签的任务。
  • cancelUniqueWork(uniqueWorkName:String):取消惟一任务。

2.4WorkStatus

当WorkManager把任务加入队列后,会为每一个WorkRequest对象提供一个LiveData(若是这个东东不了解的话赶忙去学)。 LiveData持有WorkStatus;经过观察该 LiveData, 咱们能够肯定任务的当前状态, 并在任务完成后获取全部返回的值。

val liveData: LiveData<WorkStatus> = WorkManager.getInstance().getStatusById(request.id) 

咱们来看这个WorkStatus到底都包涵什么,咱们点进去看它的源码:

public final class WorkStatus { private @NonNull UUID mId; private @NonNull State mState; private @NonNull Data mOutputData; private @NonNull Set<String> mTags; public WorkStatus( @NonNull UUID id, @NonNull State state, @NonNull Data outputData, @NonNull List<String> tags) { mId = id; mState = state; mOutputData = outputData; mTags = new HashSet<>(tags); } 

咱们须要关注的只有StateData这两个属性,首先看State:

public enum State { ENQUEUED,//已加入队列 RUNNING,//运行中 SUCCEEDED,//已成功 FAILED,//已失败 BLOCKED,//已刮起 CANCELLED;//已取消 public boolean isFinished() { return (this == SUCCEEDED || this == FAILED || this == CANCELLED); } } 

这特么又一个枚举。看过代码以后,State枚举其实就是用来给咱们作最后的结果判断的。可是要注意其中有个已挂起BLOCKED,这是啥子状况?经过看它的注释,咱们得知,若是WorkRequest的约束没有经过,那么这个任务就会处于挂起状态。

接下来,Data固然就是咱们在任务中doWork的返回值了

看到这里,我感受谷歌大佬的设计思惟仍是很是之强的,把状态和返回值同时输出,很是方便咱们作判断的同时来取值,而且这样的设计就能够达到‘屡次返回’的效果,有时间必定要去看一下源码,先立个flag!

3. 任务链

在不少场景中,咱们须要把不一样的任务弄成一个队列,好比在用户注册的时候,咱们要先验证手机短信验证码,验证成功后再注册,注册成功后再调登录接口实现自动登录。相似这样类似的逻辑比比皆是,实话说笔者之前都是在service里面用rxjava来实现的。可是如今service在Android8.0版本以上系统不能用了怎么办?固然仍是用咱们今天学到的WorkManager来实现,接下来咱们就一块儿看一下WorkManager的任务链。

3.1链式启动-并发

val request1 = OneTimeWorkRequestBuilder<MyWorker1>().build() val request2 = OneTimeWorkRequestBuilder<MyWorker2>().build() val request3 = OneTimeWorkRequestBuilder<MyWorker3>().build() WorkManager.getInstance().beginWith(request1,request2,request3) .enqueue() 

这样等同于WorkManager把一个个的WorkRequest enqueue进队列,可是这样写明显更整齐!同时队列中的任务是并行的。

3.2 then操做符-串发

val request1 = OneTimeWorkRequestBuilder<MyWorker>().build() val request2 = OneTimeWorkRequestBuilder<MyWorker>().build() val request3 = OneTimeWorkRequestBuilder<MyWorker>().build() WorkManager.getInstance().beginWith(request1) .then(request2) .then(request3) .enqueue() 

上述代码的意思就是先1,1成功后再2,2成功后再3,这期间若是有任何一个任务失败(返回Worker.WorkerResult.FAILURE),则整个队列就会被中断。

在任务链的串行中,也就是两个任务使用了then操做符链接,那么上一个任务的返回值就会自动转为下一个任务的参数!

3.3 combine操做符-组合

如今咱们有个复杂的需求:共有A、B、C、D、E这五个任务,要求AB串行,CD串行,但两个串之间要并发,而且最后要把两个串的结果汇总到E。

咱们看到这种复杂的业务逻辑,每每都会吓一跳,可是牛X的谷歌提供了combine操做符专门应对这种奇葩逻辑,不得不说:谷歌是我亲哥!

val chuan1 = WorkManager.getInstance()
    .beginWith(A)
    .then(B) val chuan2 = WorkManager.getInstance() .beginWith(C) .then(D) WorkContinuation .combine(chuan1, chuan2) .then(E) .enqueue() 

4. 惟一链

什么是惟一链,就是同一时间内队列里不能存在相同名称的任务。

val request = OneTimeWorkRequestBuilder<MyWorker>().build() WorkManager.getInstance().beginUniqueWork("tag",ExistingWorkPolicy.REPLACE,request,request,request) 

从上面代码咱们能够看到,首先与以前不一样的是,此次咱们用的是beginUniqueWork方法,这个方法的最后一个参数是一个可变长度的数组,那就证实这必定是一根链条。

而后咱们看这个方法的第一个参数,要求输入一个名称,这个名称就是用来标识任务的惟一性。那若是两个不一样的任务咱们给了相同的名称也是能够的,可是这两个任务在队列中只能存活一个。

最后咱们再来看第二个参数ExistingWorkPolicy,点进去果真又双叒是枚举:

public enum ExistingWorkPolicy { REPLACE, KEEP, APPEND } 
  • REPLACE:若是队列里面已经存在相同名称的任务,而且该任务处于挂起状态则替换之。
  • KEEP:若是队列里面已经存在相同名称的任务,而且该任务处于挂起状态,则什么也不作。
  • APPEND:若是队列里面已经存在相同名称的任务,而且该任务处于挂起状态,则会缓存新任务。当队列中全部任务执行完毕后,以这个新任务作为序列的第一个任务。

6、总结

看到这里相信你们对于WorkManager的基本用法已经了解的差很少了吧!笔者对WorkManager的了解也还不够多,欢迎你们多多留言交流!

另外经过此次对WorkManager的学习,咱们也看到官方在代码里面也仍旧在用一些他本身不推荐使用的东西,好比HashMapHashSetEnum等,只许州官放火不准百姓点灯?这很谷歌!其实不是的,所谓万事无绝对,只要你够自信,本身作好取舍,掌握平衡,用什么仍是由你本身作主!

相关文章
相关标签/搜索