Android WorkManager

1、概述

在 I / O '18中,Google发布了Android Jetpack。它是一组库,工具和架构指南,可帮助我们快速轻松地构建出色的Android应用程序。在这款Android Jetpack中,Google团队发布了一个专门用于安排和管理后台任务的库。它被称为“ WorkManager ”。WorkManager会考虑到操作系统电池优化功能(如Doze,待机等)的限制,在任何情况下(包括启动它的应用已经退出,甚至设备重启)任然承诺保证执行工作,并且它有自己的数据库来维护任务。此外,很容易计划,取消和管理多个工作顺序和平行的执行。下图是一张其总的架构图:

WorkManager架构图

2、WorkManager执行流程

整个WorkManager的执行流程如下图所示:

WorkManager执行流程

① 给WorkManager发送工作请求WorkRequest。

② WorkManager将该请求的相关参数放入WorkManager的数据库中。

③ WorkManager根据设备版本、是否是前台任务等情况将请求操作传递给JobScheduler或者AlarmManager等部件。

④ 检查Worker是否满足约束条件,当满足约束条件时调用执行Worker。

3、核心类和相关操作

3.1 核心类概述

3.1.1 Worker

Worker是一个抽象类,这个类用来指定具体需要执行的任务。使用时要继承这个类并且实现里面的doWork()方法,在其中写具体的业务逻辑。

3.1.2 WorkRequest

代表一项任务请求。一个 WorkRequest对象至少要指定一个Worker类。同时,还可以向WorkRequest对象添加 ,指定任务应运行的环境等。每个人WorkRequest都有一个自动生成的唯一ID, 可以使用该ID来执行诸如取消排队的任务或获取任务状态等操作。WorkRequest是一个抽象类; 有两个直接子类 OneTimeWorkRequest和 PeriodicWorkRequest。与WorkerRequest相关的有如下两个类:

① WorkRequest.Builder:用于创建WorkRequest对象的助手类 ,其有两个子类OneTimeWorkRequest.Builder和 PeriodicWorkRequest.Builder,分别对应两者创建上述两种WorkerRequest。

② Constraints:指定任务运行时的限制(例如,“仅在连接到网络时才能运行”)。可以通过 Constraints.Builder来创建该对象,并在调用WorkRequest.Builder的build()方法之前,将其传递 给WorkerRequest。

3.1.3 WorkManager

这个类用来安排和管理工作请求。前面创建的WorkRequest 对象通过WorkManager来安排的顺序。 WorkManager调度任务的时候会分散系统资源,做好类似负载均衡的操作,同时会遵循前面设置的对任务的约束条件。

3.1.4 WorkStatus

每一个WorkRequest都会有一个WorkStatus与之对应,里面包含了该任务的许多信息,可以通过WorkManager来获取包含WorkStatus的LiveData对象。开发者可以通过观察该LiveData对象来监听与之对应的任务所处的状态,并在任务完成后通过调用WorkStatus的getOutputData()方法获取返回值。

3.2 典型的使用

3.2.1 创建Worker

首先,创建自己的Worker类,并覆盖它的 doWork()方法,并更具情况返回执行结果状态。如下所示:


public class MyWorker extends Worker {

    @Override

    public Worker.Result doWork() {

        // 执行业务逻辑

        doSomething();

        // 根据执行情况进行返回,

        // 返回SUCCESS代表任务执行成功

        // 返回FAILURE代表任务执行失败,并且此时不会再重新执行任务

        // 返回RETRY,WorkManager会在之后再次尝试执行任务。

        return Result.SUCCESS;

    }

}

3.2.2 创建Constraints 和 WorkerRequest

如果有必要,可以指定任务运行时的限制。例如,想要指定该任务只应在设备闲置并接通电源时运行。之后根据前面创建的Worker创建WorkerRequest,并将任务约束Constraints 传递给它:


// 创建一个WorkerRequest的任务约束

Constraints myConstraints = new Constraints.Builder()

    .setRequiresDeviceIdle(true)

    .setRequiresCharging(true)

    // 还有许多其他任务约束可以添加

    .build();

// 创建一个OneTimeWorkRequest  并将上面的任务约束传给它。

OneTimeWorkRequest myWorkRequest =

                new OneTimeWorkRequest.Builder(CompressWorker.class)

    .setConstraints(myConstraints)

    .build();

3.2.3 WorkManager运行任务并监听结果

获取WorkManager并选择合适的时间来运行任务。如果需要检查任务状态,就可以通过WorkManager来获取WorkerRequest的WorkStatus句柄来获取WorkStatus对象,同时可以对该对象进行监听。


WorkManager.getInstance().enqueue(myWorkRequest );

// 通过WorkerRequest的id来获取其LiveData<WorkStatus>

WorkManager.getInstance().getStatusById(myWorkRequest .getId())

    .observe(lifecycleOwner, workStatus -> {

        //当workStatus状态改变时候可以根据业务需要进行操作

        if (workStatus != null && workStatus.getState().isFinished()) {

            // 如果任务执行完毕,可以在这里进行操作

        }

    });

3.2.4 取消任务

WorkerRequest排入队列后,可以取消任务。可以通过id或者tag进行对应任务的取消。WorkManager会尽最大努力取消任务,但这本质上是不确定的,有可能在尝试取消任务时,任务可能已经在运行了或者已经运行完成了。


UUID compressionWorkId = compressionWork.getId();

WorkManager.getInstance().cancelWorkById(compressionWorkId);

3.3 其他操作

WorkManager 除了上面的一些基本操作外。还提供了一些其他功能,可以让设置更多精细的请求。

3.3.1 重复执行任务

创建前面WorkRequest的时候用的是OneTimeWorkRequest ,代表其只执行一次,WorkRequest还有另外一个子类 PeriodicWorkRequest,可以用它来定时循环执行任务。要创建循环任务,可以使用 PeriodicWorkRequest.Builder该类创建一个 PeriodicWorkRequest对象,然后PeriodicWorkRequest按照与OneTimeWorkRequest对象相同的方式入队 。


// 第二个参数是间隔时间,第三个参数是第二个参数的单位

new PeriodicWorkRequest.Builder myPeriodicBuilder =

        new PeriodicWorkRequest.Builder(MyWorker.class, 12,

                                        TimeUnit.HOURS);

// ...如果有必要,在这里可以给builder加上约束..

PeriodicWorkRequest myPeriodicWork = myPeriodicBuilder .build();

WorkManager.getInstance().enqueue(myPeriodicWork );

3.3.2 链接任务

有时候可能需要按特定顺序运行多个任务。 WorkManager允许开发者创建和排队指定多个任务的工作序列,以及设置他们相应的运行顺序。

例如,假设有三个 OneTimeWorkRequest对象:workA,workB,和 workC。这些任务必须按照A,B,C 顺序依次运行。要入队它们,用WorkManager.beginWith() 方法创建一个序列 ,传递第一个OneTimeWorkRequest对象; 该方法会返回一个WorkContinuation对象,可以通过它来依次按顺序添加剩余的OneTimeWorkRequest,最后将整个序列排入 WorkContinuation.enqueue():


WorkManager.getInstance()

    // 首先运行A类任务

    .beginWith(workA1, workA2, workA3)

    // 当所有A类任务运行完毕再运行B类任务

    .then(workB)

    // 接着再运行C类任务

    .then(workC1, workC2)

    .enqueue();

也可以通过使用WorkContinuation.combine() 方法连接多个链来创建更复杂的序列 。例如,假设要运行如下序列:

链式任务

建立这个序列,创建两个单独的链,然后将它们连接在一起成为第三个链:


WorkContinuation chain1 = WorkManager.getInstance()

    .beginWith(workA)

    .then(workB);

WorkContinuation chain2 = WorkManager.getInstance()

    .beginWith(workC)

    .then(workD);

WorkContinuation chain3 = WorkContinuation

    .combine(chain1, chain2)

    .then(workE);

chain3.enqueue();

虽然WorkManager每个子链的运行有序,但是chain1 和 chain2之间的运行顺序就无法保证了。例如,workB可能在workC之前或之后运行,或者它们可能同时运行。能保证的是每个子链中的任务将按顺序运行; 也就是说,workB直到workA 完成后才开始。combine()方法还能这么用WorkContinuation.combine(OneTimeWorkRequest, WorkContinuation…)也就是链和单个WorkRequest的结合,详情参见官方文档:WorkContinuation文档(要梯子)

3.3.3 唯一工作序列

可以创建一个唯一的工作序列,通过调用函数 beginUniqueWork() 开始而不是beginWith()。每个唯一的工作序列都有一个名字; WorkManager只允许一个具有该名称的工作序列存在。当创建一个新的唯一工作序列时,WorkManager如果已经有一个待处理的序列具有相同的名字会根据传入的策略标志不同有如下三种操作:

① KEEP:保留现有序列并忽略新来的序列

② REPLACE:取消现有的序列并将其替换为新序列

③ APPEND:将新序列附加到现有序列,在现有序列的最后一个任务完成后运行新序列的第一个任务。

如果任务不应多次排队,就可以使用唯一工作序列。例如,如果想把数据同步到网络上,可以入队一个名为“同步”的序列,并将策略标志传入KEEP,这样在此期间新的同步工作请求就都会被忽略了。

3.2.4 设置标签

可以为任何WorkRequest对象分配标记字符串来对任务进行分组 。要设置标签,可调用WorkRequest.Builder.addTag()方法,例如:


OneTimeWorkRequest myWorkRequest=

        new OneTimeWorkRequest.Builder(MyWorker.class)

    .setConstraints(myConstraints)

    .addTag("myWork")

    .build();

WorkManager类提供了几种实用方法,可以使用特定标签对所有任务进行操作。例如 WorkManager.cancelAllWorkByTag() 取消具有特定标记的所有任务,WorkManager.getStatusesByTag() 返回具有该标记的所有任务的WorkStatus。

3.3.5 输入和输出

为了获得更大的灵活性,可以将参数传递给任务,并让任务返回结果。传递和返回的值是键值对。要将参数传递给任务,在创建WorkRequest 对象之前调用WorkRequest.Builder.setInputData() 。该方法传入Data对象,其通过Data.Builder进行创建。Worker类可以通过调用Worker.getInputData()来访问这些参数 。要输出返回值,任务调用 Worker.setOutputData(),该方法的参数也是一个Data对象; 可以通过观察任务的LiveData<WorkStatus>获得该输出。这边还要说一下对于传入的参数目前只能是一些int boolean String的基本数据和对应的数组结构,而且总大小要小于10k,不能传入对象。

下面是定义一个Worker 类的例子:


// 定义 Worker 类:

public class MathWorker extends Worker {

    // 定义传入参数的key:

    public static final String KEY_X_ARG = "X";

    public static final String KEY_Y_ARG = "Y";

    public static final String KEY_Z_ARG = "Z";

    // ...定义输出参数的key:

    public static final String KEY_RESULT = "result";

    @Override

    public Worker.Result doWork() {

        // 抓取传入参数和如果设置抓取失败的默认值

        int x = getInputData().getInt(KEY_X_ARG, 0);

        int y = getInputData().getInt(KEY_Y_ARG, 0);

        int z = getInputData().getInt(KEY_Z_ARG, 0);

        // ...do something..

        int result = ...

        //...设置输出参数

        Data output = new Data.Builder()

            .putInt(KEY_RESULT, result)

            .build();

        setOutputData(output);

        return Result.SUCCESS;

    }

}

入队上述的worker类并传入参数:


// 创建一个Data 对象:

Data myData = new Data.Builder()

    // 传入输入值

    .putInt(KEY_X_ARG, 42)

    .putInt(KEY_Y_ARG, 421)

    .putInt(KEY_Z_ARG, 8675309)

    // ...调用build()进行创建:

    .build();

// ..创建并入队一个任务并给这个任务设置一个输入参数

OneTimeWorkRequest mathWork = new OneTimeWorkRequest.Builder(MathWorker.class)

        .setInputData(myData)

        .build();

WorkManager.getInstance().enqueue(mathWork);

输出值会存在对应WorkRequest的WorkStatus中:


WorkManager.getInstance().getStatusById(mathWork.getId())

    .observe(lifecycleOwner, status -> {

        if (status != null && status.getState().isFinished()) {

          int myResult = status.getOutputData().getInt(KEY_RESULT,

                  myDefaultValue));

          // ...根据返回值做相应处理 ...

        }

    });

如果是一个链接任务,则前一个任务的输出可用作链中下一个任务的输入。如果是一个简单的单任务链,前一个任务通过调用setOutputData()返回结果 ,后一个任务通过调用getInputData()来获取结果 。如果链更复杂, 例如,因为几个任务都将输出发送到单个后续任务,就需要通过InputMerger来处理了。

3.3.6 合并输入参数

前面已经说到了,前一个任务的输出可用作链中下一个任务的输入。但是当遇到如下情况就有个问题:

一个work有多个前置work

如上图所示,当一个任务有两个前置任务时,这是后直接使用前面输出的output Data,就要给它设置InputMerger策略。来对work1 的output 和work2 的output 进行合并。合并策略现在有如下两种

3.3.6.1 OverwritingInputMerger

这个策略是用来将前面的output 进行覆盖合并,如果两个output 有相同的key,则后者会将前者覆盖,有key 就给input新加一组key,value对。

第一组数据传入input
第二组覆盖新增之前的

3.3.6.2 ArrayCreatingInputMerger

这个策略是用来将前面的output 进行全部保留合并,如果两个output 有相同的key,则会同时保留两者的数据,有key 就给input新加一组key,value对。如果有相同的key但是是不同的数据类型,则会抛出异常。

合并两组数据

使用时候给接收数据的WorkRequest传入对应的策略就好了:


OneTimeWorkRequest mathWork = new OneTimeWorkRequest.Builder(MathWorker.class)

        .setInputMerger(OverwritingInputMerger.class)

        .build();

4、WorkManager的存在意义。

关于WorkManager的使用上面已经讨论完毕了,接下来想讨论下WorkManager存在的意义。

4.1 WorkManager和Doze模式

将任务在后台运行,首先会想到Android的四大组件之一的Service。Service官方对其的定义是这么说的:Service是一个应用程序组件,可以在后台执行长时间运行的操作,并且不提供用户界面。一般来说如果我们想在后台开启一个长时间的操作,最好通过Service来启动一个线程进行,而不是直接在Activity中启动,因为当Activity被销毁时,一旦内存空间不足,这个线程就会很容易被杀死。但通过Service启动就不一样了,开发者只要不手动关闭Service,这个线程也就不容易被Android系统杀死了。而这也成为了Service招黑的一个点,开发者如果进行了一个5分钟让服务从后台获取数据的操作,那么Android的电会很快耗干净。就是因为开发者对于Android系统的为所欲为,从Android6.0开始,推出了Doze模式:

Doze模式

该模式简而言之就是在用户关闭设备屏幕后,Doze模式启动并禁用网络,同步,GPS,警报和wifi扫描等操作,禁用这些操作。它一直保持到用户打开屏幕或连接充电器,同时Android会在Doze模式下每隔一段时间就放开这些禁锢,让这些禁用的操作能够集中在这一小段时间内进行(这样可以减少唤醒次数,每次唤醒手机还是很耗费电量的)。这样就一方面可以节省电池,一方面也可以保证一些后台任务有机会执行。很可惜的是这个模式并不兼容Service,为了用户的电池着想后台Service要被抛弃了。Android也是这么做的,从Android8.0开始,如果想通过Service的startService()方法创建后台服务,会抛出IllegalStateException异常。

而WorkManager、JobScheduler等后台操作则是很完美的兼容了Doze模式。

4.2 WorkManager和其他后台任务的比较和关系

其实在WorkManager出来之前,已经有许多兼容Doze模式的后台操作了,比如JobScheduler、Alarm Manager等。其实从最前面我放的一张图可以看到其实WorkManager可以说是这些后台调度操作的集大成者。具体关系如下图:

WorkManager调用其他后台任务关系

WorkManager支持API14或更高的版本,它会根据设备API级别和应用程序状态等因素选择适当的方式来运行任务。如果调用WorkManager的应用程序正在运行,那就直接创建一个新的线程来运行任务。否则看这个任务是不是立刻运行(没有设置约束条件),是的话就用Alarm manager运行,否则就看API版本是否高于21,高于21的用JobScheduler来运行任务,低于的情况下,如果有Google 服务(中国就没有!!!)就用Firebase Job Dispatcher,没有的话用Alarm manager。

从上面的表述,其实可以看到其他符合Doze模式的调度操作,或多或少有不尽如人意的地方,不是API支持的少了(JobScheduler),就是需要Google的服务(Firebase Job Dispatcher),又或者无法支持义约束条件或者自定义重启策略(WorkManager)。如果没有WorkManager,如果开发者想开发出健壮的后台调用程序,就必须自己去做这些处理。那无疑是一件很麻烦的事情。

到此WorkManager存在意义已经梳理的差不多了,首先它好用,前面已经说明了它的各种方便的操作。其次它支持Doze模式,能省电。最后就是能够根据实际情况,尽可能的兼容各种版本和应用程序状态。

Tips:说了WorkManager的各种好,但是就目前我自己使用的经验来讲还是有一些不足之处的,比如它不能输入输出对象,比如它没有任务执行进度回调接口(WorkManager团队说考虑会在之后的版本加上)。

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

推荐阅读更多精彩内容