07 WorkManager组件架构应用

前言

前面的内容中我们已经介绍了很多Jetpack中的架构组件,可以说每一种组件的出现都是为了更好的解决现在存在的问题。同样的,WorkManager的出现也是为了解决某一问题,试想这样一个场景:在做开发的时候总避免不了做些后台任务,好比我们的业务埋点的上传,正常我们用定时器也可以实现,但是一旦用户退出了应用,我们的应用到了后台,一旦遇到系统内存吃紧的情况,应用进程就会被杀掉了,遇到这种情况我们的埋点就没法上传了。

但是WorkManager的出现基本解决了这种问题,它可以在应用退出的时候依然执行一些异步任务。

用官方的话来讲就是:WorkManager旨在用于可延迟运行(即不需要立即运行)并且在应用退出或设备重启时必须能够可靠运行的任务。

本篇小节
  • 什么是WorkManager
  • 如何使用WorkManager
    -WorkManager与Service
  • WorkManager工作原理
什么是WorkManager?

WorkManager是新一代后台任务的管理者,可以以轻松简单的方式实现复杂的后台任务控制。大量应用程序都有在后台执行任务的需求。根据需求的不同,Android为后台任务提供了多种解决方案,如JobScheduler,Loader,Service等。如果这些API没用被适当地使用,可能会消耗大量的电量。Android在解决应用程序耗电问题上做了各种尝试,从Doze到App Standby,通过各种方式限制和管理应用程序,以保证应用程序不会在后台过量消耗设备电量。WorkManager的出现,则是为应用程序中那些不需要及时完成的任务,提供统一的解决方案,以便在设备电量和用户体验之间达到一个比较好的平衡。

WorkManager的优势
  • Workmanager确保任务可以被执行,在条件满足的时候会即便应用程序不在运行中也可以执行任务
  • WorkManager拥有丰富的任务控制手段和任务状态反馈,它提供多种方式控制任务是否继续执行,任务运行的每种状态都会以观察者的形式反馈;
  • 多任务串联,例如执行任务A之前需要任务B和C先行完成,将会非常方便;
  • 向前兼容支持到Api14,但会受到系统后台任务的限制管理以节省电量,例如APP进行Doze Mode的时候,任务将不会被执行。
如何使用WorkManager?

老生常谈了,使用WorkManager之前同样要先添加依赖:

implementation "androidx.work:work-runtime:2.4.0"
WorkManager的关键几个类介绍

由于该组件设计到的类比较多,所以在使用Work之前我们先来看一下Work组件中的几个关键类:

  • Worker:任务的执行者,是一个抽象类,需要继承它实现要执行的任务;
  • WorkRequest:每个任务在执行时都需要对构建一个WorkRequest对象,才能加入队列。有两个常用的子类OneTimeWorkRequest(任务只执行一遍)、PeriodicWorkRequest(任务周期性的执行)。
  • WorkManager:管理任务请求和任务队列,发起的WorkRequest会进行它的任务队列;
  • WorkStatus: 包含有任务的状态和任务的信息,以LiveData的形式提供给观察者。
执行文件上传的任务

文件上传的场景每个APP都会遇到,按照之间的写法,是非常麻烦的。但如果使用WorkManager将会非常轻松。

第一步,构建执行任务的类UploadFileWork

public class UploadFileWorker extends Worker {
    //需要让他继承自SDK提供的Worker父类
    public UploadFileWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
        //任务的执行是在这里完成的,注意这里已经是运行在子线程里面的了
        //getInputData方法可以获取 执行任务时传递的参数Data对象,本质是hashMap的进一步封装,但是Data类传递的参数不能超过10Kb,而且只支持基本数据类型和他们的数组。
        Data inputData = getInputData();
        //从入参中取出文件路径
        String filePath = inputData.getString("file");
        //执行文件上传,得到文件的Url
        String fileUrl = FileUploadManager.upload(filePath);
        
        //任务执行完,无论是成功失败,都需要返回Result对象。
        //此时它可以接收一个Data对象。用来承载文件上传得到的url
        Data outputData = new Data.Builder().putString("fileUrl", fileUrl)
                    .build();
        return Result.success(outputData);  //Result.failure()
    }
}

第二步,构建WorkRequest,可以使用下面的这两个子类来构建不同的Request对象

  • OneTimeWorkRequest类:用以构建一次性任务的Request;
  • PeriodicWorkRequest类:用以构建周期性执行的任务Request,任务间隔最少是15分钟,所以保活就别想了。

在构建Request对象的时候,提供了非常多的可选参数,可参考下面每个参数的作用。其中Builder(UploadFileWorker.class)必须指定。

private OneTimeWorkRequest makeOneTimeWorkRequest(String filePath) {
         //构建任务的入参对象
        Data inputData = new Data.Builder()
                .putString("file", filePath)
                .build();
         //在构建request对象的时候,提供了非常多的参数,可以参考下面每个参数的作用
//       Constraints constraints = new Constraints();
//        //设备存储空间充足的时候 才能执行 ,>15%
//        constraints.setRequiresStorageNotLow(true);
//        //必须在执行的网络条件下才能好执行,不计流量 ,wifi
//        constraints.setRequiredNetworkType(NetworkType.UNMETERED);
//        //设备的充电量充足的才能执行 >15%
//        constraints.setRequiresBatteryNotLow(true);
//        //只有设备在充电的情况下 才能允许执行
//        constraints.setRequiresCharging(true);
//        //只有设备在空闲的情况下才能被执行 比如息屏,cpu利用率不高
//        constraints.setRequiresDeviceIdle(true);
//        //workmanager利用contentObserver监控传递进来的这个uri对应的内容是否发生变化,当且仅当它发生变化了我们的任务才会被触发执行,
//        以下三个api是关联的
//        constraints.setContentUriTriggers(Uri.from('file'));
//        //设置从content变化到被执行中间的延迟时间,如果在这期间。content发生了变化,延迟时间会被重新计算
        //这个content就是指 我们设置的setContentUriTriggers uri对应的内容
//        constraints.setTriggerContentUpdateDelay(0);
//        //设置从content变化到被执行中间的最大延迟时间
        //这个content就是指 我们设置的setContentUriTriggers uri对应的内容
//        constraints.setTriggerMaxContentDelay(0);
      
        OneTimeWorkRequest request = new OneTimeWorkRequest
                 //执行任务的class对象
                .Builder(UploadFileWorker.class)
                //传递任务执行的入参
                .setInputData(inputData)
//                .setConstraints(constraints)
//                //设置一个拦截器,在任务执行之前 可以做一次拦截,去修改入参的数据然后返回新的数据交由worker使用
//                .setInputMerger(null)
//                //当一个任务被调度失败后,所要采取的重试策略,可以通过BackoffPolicy来执行具体的策略
//                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
//                //任务被调度执行的延迟时间
//                .setInitialDelay(10, TimeUnit.SECONDS)
//                //设置该任务尝试执行的最大次数
//                .setInitialRunAttemptCount(2)
//                //设置这个任务开始执行的时间  System.currentTimeMillis()
//                .setPeriodStartTime(0, TimeUnit.SECONDS)
//                //当一个任务执行状态变成finish时,又没有后续的观察者来消费这个结果,难么workamnager会在
//                //内存中保留一段时间的该任务的结果。超过这个时间,这个结果就会被存储到数据库中
//                //下次想要查询该任务的结果时,会触发workmanager的数据库查询操作,可以通过uuid来查询任务的状态
//                .keepResultsForAtLeast(10, TimeUnit.SECONDS)
                .build();
        return request;
    }

第三步,把任务加入工作队列

//1.把任务加入队列,在合适的时机会被执行
WorkManager.getInstance().enqueue(request)

//2.如果多个任务之间存在依赖关系,此时可以使用如下方式
//此时 是requestA 和 requestB都执行完才会去执行requestC,requestC执行完才会去执行requestD
WorkContinuation chain = WorkManager.getInstance(application).beginWith(requestA,requestB);
chain.then(requestC).then(requestD)
chain.enqueue();

//3.再来个复杂点的  requestA->requestB--\
//                                   >>> requestE.
//                requestC->requestD--/  
//这个场景是 A,C同时并发执行,A执行完执行B,C执行完执行D,BD同时执行完才执行E.
val chainAB = WorkManager.getInstance()
        .beginWith(requestA)
        .then(requestB)
val chainCD = WorkManager.getInstance()
        .beginWith(requestC)
        .then(requestD)
val chain = WorkContinuation
        .combine(chainAB, chainCD)
        .then(requestE)
chain.enqueue()
//可以看到尽管任务链关系很复杂,但是workmanager可以轻易实现。
任务状态监听

每个任务加入队列之后,都会有以下7种状态:

  • BLOCKED:任务阻塞中;
  • ENQUEUED:队列等待中;
  • RUNNING:任务执行中;
  • SUCCESSED:任务执行成功;
  • CANCELED:任务取消;
  • FAILED:任务失败;
  • FINISHED:任务结束。
//根据UUID获取任务  并监听它的状态。uuid可以通过request.getId()得到。
//可以把该uuid存储本地,以至于在任意页面都可以查询任务的执行状态
 WorkManager.getInstance(this).getWorkInfoById(uuid).observer()
 //根据任务的tag获取一组任务,并监听他们的状态
 WorkManager.getInstance(this).getWorkInfosByTag().observer()
 
//得到队列中所有的任务 并获取他们的状态
workContinuation.getWorkInfosLiveData().observe(PublishActivity.this, new Observer<List<WorkInfo>>() {
            @Override
            public void onChanged(List<WorkInfo> workInfos) {
                //任务的状态有六种分别是:
                //block runing enuqued failed susscess finish
                for (WorkInfo workInfo : workInfos) {
                    WorkInfo.State state = workInfo.getState();
                    Data outputData = workInfo.getOutputData();
                    UUID uuid = workInfo.getId();
                    if (state == WorkInfo.State.FAILED) {
                        // if (uuid==coverUploadUUID)是错的,
                        //coverUploadUUID = request,getId()得到
                        if (uuid.equals(coverUploadUUID)) {
                           
                        }  else if (state == WorkInfo.State.SUCCEEDED) {
                           
                        }
                      
            }
        });
任务控制
  • 取消所有任务;
WorkManager.getInstance(this).cancelAllWork()
  • 根据任务的tag取消,如果多个任务拥有同一个tag,那么它们都会被取消;
WorkManager.getInstance(this).cancelAllWorkByTag("tag")
  • 根据任务名称取消任务;
WorkManager.getInstance(this).cancelUniqueWork("uploadfile")
  • 根据任务的UUID取消某个任务。
WorkManager.getInstance(this).cancelWorkById()
WorkManager与Service

WorkManager区别于异步任务,它更像是一个系统级的后台服务。基本上,Workmanager能做的,Service也能做;但反观Service,泛滥的Service后台任务可能是引起Android系统卡顿的主要原因,这几年Google也对Service也做了一些限制:

  • 休眠模式:Android 6.0(API 23)在关闭手机屏幕后,系统会禁止应用的后台任务网络请求等功能;
  • 后台启动:Android 8.0(API 26)在后台不允许启动Service,会抛异常。

而WorkManager作为一个更合理的后台任务管理库,我们可以将一些诸如埋点、日志上报等费劲及执行的后台任务转义到WorkManager来实现。

WorkManager工作原理

我们使用WorkManager构建的这些任务,在加入任务队列之后都会被保存到数据库,所以哪怕程序退出了,这些任务也能够被执行。如果在调度任务时我们的APP在运行中,此时会选择线程池来执行,如果在调度任务时我们的APP没有运行,在5.0及以上版本会选择JobScheduler来调度,5.0以下会使用AlarmManager来调度任务。任务执行的状态和结果都会被持久化到数据库。APP下次启动时,也可以去查询任务的执行结果。

总结

对于需要立刻执行的任务还是不能使用WorkManager来调度,因为WorkManager的任务调度会收到系统当前状态的影响。虽然官方说即便应用退出了,也能保障任务得到执行,这一点在模拟器和Pixel手机上是得到验证的。但是国内手机Rom版本众多,还需要亲自验证才能知道。

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