WorkManager

本篇文章完全转载于微笑的江豚 的博客地址:

https://my.oschina.net/JiangTun
如有问题,请及时联系!

前言

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

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

谷歌专项整顿

  • 6.0 (API 级 23) 引入了 Doze 机制和应用程序待机。当屏幕关闭且设备静止时, 打盹模式会限制应用程序的行为。应用程序待机将未使用的应用程序置于限制其网络访问、作业和同步的特殊状态。
  • Android 7.0 (API 级 24) 有限的隐性广播和 Doze-on-the-go.
  • 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!

官方的推荐(qiangzhi)做法

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

WorkManager的推出

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

也就是说,WorkManager 可以自动维护后台任务,同时可适应不同的条件,同时满足后台Service 和静态广播,内部维护着 JobScheduler,而在6.0以下系统版本则可自动切换为AlarmManager,Amazing!

WorkManager详解

引入
implementation "android.arch.work:work-runtime:1.0.0-alpha06" // use -ktx for Kotlin
implementation "android.arch.work:work-runtime:1.0.0-alpha01"
重要的解析类
  • 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
    }
}
向任务添加参数

在Request中传参

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

在 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 任务成功。

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

WorkRequest

也是一个抽象类,可以对 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组。这样的好处是以后可以直接控制整个组就行了,组内的每个成员都会受到影响。

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):取消唯一任务。
WorkStatus

当 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);
    }

我们需要关注的只有 State 和 Data 这两个属性,首先看 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!

任务链

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

  • 链式启动-并发
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 进队列,但是这样写明显更整齐!同时队列中的任务是并行的。

  • 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 操作符连接,那么上一个任务的返回值就会自动转为下一个任务的参数!

  • 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()
唯一链

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

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:如果队列里面已经存在相同名称的任务,并且该任务处于挂起状态,则会缓存新任务。当队列中所有任务执行完毕后,以这个新任务做为序列的第一个任务。

总结

看到这里相信大家对于 WorkManager 的基本用法已经了解的差不多了吧!

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,479评论 2 59
  • 1、概述 在 I / O '18中,Google发布了Android Jetpack。它是一组库,工具和架构指南,...
    高丕基阅读 7,378评论 1 12
  • 01 小狗钱钱引发的思考 群主大人分享小狗钱钱音频,然后群里就热闹了起来,有的说“女儿学书法,鼓励她教其他同学,在...
    陈念媛阅读 147评论 0 0
  • 一部豆瓣评分9.0的电影,又是徐峥主演的,没有看影评,直接冲着口碑去的。如此高的口碑和呼声之下,究竟给我的感觉是震...
    柠绾绾阅读 310评论 0 3