Android Jetpack系列(八):WorkManager

作者: 程序老秃子 | 来源:发表于2022-08-04 14:35 被阅读0次

    前言

    WorkManager是Jetpack很重要的一个组件; 本篇我们就先来讲讲它是如何使用的,在讲解之前我们先了解关于后台处理的一些痛点

    后台处理指南

    我们知道每个 Android 应用都有一个主线程,它负责处理界面(包括测量和绘制视图)、协调用户互动以及接收生命周期事件; 如果有太多工作在主线程中进行,则应用可能会挂起或运行速度变慢,从而导致用户体验不佳。任何长时间运行的计算和操作(例如解码位图、访问磁盘或执行网络请求)都应在单独的后台线程上完成

    一般来说,任何所需时间超过几毫秒的任务都应该分派到后台线程; 在用户与应用积极互动时,可能需要执行几项这样的任务;即使在用户没有积极使用应用时,应用可能也需要运行一些任务(例如,定期与后端服务器同步或定期从应用内提取新内容)

    后台处理面临的挑战

    后台任务会使用设备的有限资源,例如 RAM 和电池电量; 如果处理不当,可能会导致用户体验不佳;为了最大限度地延长电池续航时间并强制推行良好的应用行为,Android 会在应用(或前台服务通知)对用户不可见时,限制后台工作;为此Google在不同平台上逐步的改进

    Android 6.0(API 级别 23)引入了低电耗模式和应用待机模式

    低电耗模式会在未插接设备的电源,在屏幕关闭的情况下,让设备在一段时间内保持不活动状态,那么设备就会进入低电耗模式; 在低电耗模式下,系统会尝试通过限制应用访问占用大量网络和 CPU 资源的服务来节省电量。它还会阻止应用访问网络,并延迟其作业、同步和标准闹钟

    系统会定期退出低电耗模式一小段时间,让应用完成其延迟的活动; 在此维护期内,系统会运行所有待处理的同步、作业和闹钟,并允许应用访问网络。在每个维护期结束时,系统会再次进入低电耗模式,暂停网络访问并推迟作业、同步和闹钟

    随着时间的推移,系统安排维护期的次数越来越少,这有助于在设备未连接至充电器的情况下长期处于不活动状态时降低耗电量; 一旦用户通过移动设备、打开屏幕或连接至充电器唤醒设备,系统就会立即退出低电耗模式,并且所有应用都会恢复正常活动

    应用待机模式允许系统判定应用在用户未主动使用它时是否处于闲置状态; 当用户有一段时间未触摸应用时,系统便会作出此判定;当用户将设备插入电源时,系统会从待机状态释放应用,允许它们自由访问网络并执行任何待处理的作业和同步。如果设备长时间处于闲置状态,系统将允许闲置应用访问网络,频率大约每天一次

    低电耗模式和应用待机模式管理在 Android 6.0 或更高版本上运行的所有应用的行为,无论它们是否专用于 API 级别 23

    Android 7.0(API 级别 24)限制了隐式广播

    引入了随时随地使用低电耗模式; 使得低电耗模式又前进了一步,随时随地可以省电;只要屏幕关闭了一段时间,且设备未插入电源,低电耗模式就会对应用使用熟悉的 CPU 和网络限制;这意味着用户即使将设备放入口袋里也可以省电

    Android 8.0(API 级别 26)进一步限制后台行为

    例如在后台获取位置信息和释放缓存的唤醒锁定

    Android 9.0(API 级别 28)引入了应用待机存储分区

    通过它,系统会根据应用使用模式动态确定应用资源请求的优先级; 应用待机存储分区有助于系统根据应用的使用时间新近度和使用频率对应用资源请求确定优先级

    根据应用使用模式,每个应用都会被放置在五个优先级存储分区之一中; 系统会根据应用所在的存储分区限制每个应用可用的设备资源

    • Android 6.0(API 级别 23)引入了低电耗模式和应用待机模式

    低电耗模式会在屏幕处于关闭状态且设备处于静止状态时限制应用行为; 应用待机模式会将未使用的应用置于一种特殊状态,进入这种状态后,应用的网络访问、作业和同步会受到限制

    • Android 7.0(API 级别 24)限制了隐式广播,并引入了随时随地使用低电耗模式

    • Android 8.0(API 级别 26)进一步限制了后台行为,例如在后台获取位置信息和释放缓存的唤醒锁定

    • Android 9(API 级别 28)引入了应用待机存储分区,通过它,系统会根据应用使用模式动态确定应用资源请求的优先级

    如何选择合适的后台解决方案

    下面有一张图完美的解答了这个问题

    • 从上图我们可以清晰的了解如何选择后台解决方案,如果是一个长时间的http下载的话就使用DownloadManager

    • 否则的话就看是不是一个可以延迟的任务,如果不可以就使用Foreground service

    • 如果是的话就看是不是可以由系统条件触发,如果是的话就使用WorkManager

    • 如果不是就看是不是需要在一个固定的时间执行这个任务,如果是的话就使用AlarmManager

    • 如果不是的话就使用WorkManager

    WorkManager概述

    • 使用 WorkManager API 可以轻松地调度可延迟的工作以及预计即使您的设备或应用重启也会运行的工作,即使在应用退出或设备重启时仍应运行的可延迟异步任务

    • 最高向后兼容到 API 14

    • 在运行 API 23 及以上级别的设备上使用 JobScheduler

    • 在运行 API 14-22 的设备上结合使用 BroadcastReceiver 和 AlarmManager

    • 添加网络可用性或充电状态等工作约束

    • 调度一次性或周期性异步任务

    • 监控和管理计划任务

    • 将任务链接起来

    • 确保任务执行,即使应用或设备重启也同样执行任务

    • 遵循低电耗模式等省电功能

    WorkManager使用

    1 声明依赖项

    dependencies {
      def work_version = "2.3.1"
    
        // (Java only)
        implementation "androidx.work:work-runtime:$work_version"
    
        // Kotlin + coroutines
        implementation "androidx.work:work-runtime-ktx:$work_version"
    
        // optional - RxJava2 support
        implementation "androidx.work:work-rxjava2:$work_version"
    
        // optional - GCMNetworkManager support
        implementation "androidx.work:work-gcm:$work_version"
    
        // optional - Test helpers
        androidTestImplementation "androidx.work:work-testing:$work_version"
    }
    

    2 自定义一个继承自Worker的类

    重写doWork方法,或者使用协程的话,得继承自CoroutineWorker。doWork方法有一个返回值,来标记任务是否成功或者是否要retry; 返回值有三种,分别是Result.success(),Result.failure(),Result.retry()

    执行成功返回Result.success() 执行失败返回Result.failure() 需要重新执行返回Result.retry()

    override fun doWork(): Result {
        for (i in 1..3) {
            Thread.sleep(500)
            Log.i("aaa", "count: $i parameter: ${inputData.getString("parameter1")}")
        }
        return Result.success(Data.Builder().putString("result1", "value of result1").build())
    }
    

    3 选择worker执行的条件

    //添加约束
    val constraints = Constraints.Builder()
                    .setRequiredNetworkType(NetworkType.CONNECTED)
                    .setRequiresBatteryNotLow(false)
                    .setRequiresCharging(false)
                    .setRequiresDeviceIdle(false)
                    .setRequiresStorageNotLow(false)
                    .build()
      //对一次性执行添加约束,如果返回faliure或者retry的话就在适当的约束条件下执行worker
      val request = OneTimeWorkRequestBuilder<CountWorker>()
                    .setConstraints(constraints)
                    .setInputData(Data.Builder().putString("parameter1", "value of parameter1").build())
                    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS)
                    .build()
     WorkManager.getInstance(context).enqueue(request)
    
    //或者定时每隔一个小时执行任务  
    val periodicWorkRequest = PeriodicWorkRequest.Builder(AppsWorker::class.java,
                                        1, TimeUnit.HOURS)
                                         .setConstraints(constraints)
                                         .build();
     WorkManager.getInstance(context).enqueue(periodicWorkRequest)
    

    需要注意的是类似于JobSceeduler,周期性执行的任务最少间隔时间不能小于15mins

    4 下面贴出自定义worker类的全部源码

    class CountWorker(context: Context, parameters: WorkerParameters)
        : Worker(context, parameters) {
    
        companion object {
            fun enqueue(context: ComponentActivity) {
                val constraints = Constraints.Builder()
                        .setRequiredNetworkType(NetworkType.CONNECTED)
                        .setRequiresBatteryNotLow(false)
                        .setRequiresCharging(false)
                        .setRequiresDeviceIdle(false)
                        .setRequiresStorageNotLow(false)
                        .build()
                val request = OneTimeWorkRequestBuilder<CountWorker>()
                        //-----1-----添加约束
                        .setConstraints(constraints)
                        //-----2----- 传入执行worker需要的数据
                        .setInputData(Data.Builder().putString("parameter1", "value of parameter1").build())
                        //-----3-----设置避退策略
                        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.HOURS)
                        .build()
                 //-----4-----将任务添加到队列中
                //WorkManager.getInstance(context).enqueue(request)
                //或者采用uniqueName执行
                WorkManager.getInstance(context).beginUniqueWork("uniqueName", ExistingWorkPolicy.REPLACE, request).enqueue()
                //-----5-----对任务加入监听
                WorkManager.getInstance(context).getWorkInfoByIdLiveData(request.id).observe(context, Observer {
                    //-----8----获取doWork中传入的参数
                    Log.i("aaa", "workInfo ${it.outputData.getString("result1")} ${it.state}: ")
                })
                //或者采用tag的方式监听状态
                WorkManager.getInstance(context).getWorkInfosByTagLiveData("tagCountWorker").observe(context, Observer {
                Log.i("aaa", "workInfo tag-- ${it[0].outputData.getString("result1")} ${it[0].state}: ")
                })
                //或者采用uniqueName的形式监听任务执行的状态
                WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("uniqueName").observe(context, Observer {
                Log.i("aaa", "workInfo uniqueName-- ${it[0].outputData.getString("result1")} ${it[0].state}: ")
             })
            }
        }
    
        override fun doWork(): Result {
            for (i in 1..3) {
                Thread.sleep(500)
                //-----6-----获取传入的参数
                Log.i("aaa", "count: $i parameter: ${inputData.getString("parameter1")}")
            }
           //-----7-----传入返回的参数
            return Result.success(Data.Builder().putString("result1", "value of result1").build())
        }
    }
    
    • 为了测试方便,我把执行的代码写在了enqueue中了,在enqueue中,我们首先在注释1处添加了约束

    • 在注释2处添加了执行worker需要的参数。这个参数可以在doWork中获取到,如注释6处所示;传入的数据不能超过10kb

    • 注释3处我们设置了避退策略,如果我们的一次性任务返回了retry,这里就可以起作用了,避退策略有两种方式。一种是指数级的EXPONENTIAL,还有一种是线性的LINEAR

    • 然后注释4处将任务加入到队列中,这里仅仅是加入队列,并不能保证执行,因为WorkManager主要的定位就是针对可延迟的任务,它需要根据添加的约束和系统自身的情况来做出什么时间执行这个任务

    • 注释5处可以根据request的id获取到任务的执行状态,返回值是一个LiveData类型的,并将其加入到生命周期观察序列中;所以当任务的执行状态发生变化的时候就会在注释8处打印信息

    • 我们还可以在任务执行结束的时候传入需要返回的参数,但是只能在success和failure的时候传入,传入的数据可以再注释8处获取

    5 执行任务的方式

    如果我们想要以链式执行一系列任务,如图所示,我们可以使用:

     WorkManager.getInstance(context).beginWith(requestA).then(requestB).enqueue()
    

    如果我们的任务A和任务B之间没有关系,需要在任务A和B都完成的情况下执行任务C的话,如图所示,这时候就可以这么调用:

    WorkManager.getInstance(context).beginWith(listOf(requestA,requestB)).then(requestC).enqueue()
    

    如果我们想要AB和CD并行的执行完,然后执行E的话,如图所示,可以采用:

    val continuation1 = WorkManager.getInstance(context).beginWith(requestA).then(requestB)
    val continuation2 = WorkManager.getInstance(context).beginWith(requestC).then(requestD)
    WorkContinuation.combine(listOf(continuation1, continuation2)).then(requestE).enqueue()
    
    • 需要注意的是任务一旦发起,任务是可以保证一定会被执行的,就算退出应用,甚至重启手机都阻止不了他;但可能由于添加了环境约束等原因会在不确定的时间执行罢了

    6 取消任务的执行

    //通过request.id取消任务
    WorkManager.getInstance(context).cancelWorkById(request.id)
    //通过request的tag取消任务
    WorkManager.getInstance(context).cancelAllWorkByTag("tag")
    //通过request的uniqueName取消任务
    WorkManager.getInstance(context).cancelUniqueWork("uniqueName")
    //取消所有的work任务
    WorkManager.getInstance(context).cancelAllWork()
    
    • 以上可以看到可以通过四种方式取消任务

    有需要文章中完整代码的同学 现在点击此处传送门 即可免费获取

    现在点击链接还可以获取《更多 Android 源码解析+核心笔记+面试真题》

    Android 核心笔记目录:

    Android 面试真题:

    最后我想说:

    学习没有捷径可言,我们要注意记学习,不仅要记,还要写心得体会,文字笔记、画图、总结等,方式很多,但是一定要自己认真去做,不要太相信自己的记忆,只有反复记忆,加深理解才行

    同时,对于程序员而言,不单单是死记硬背,我们有更好的方式去学习,比如写demo去验证。复习知识点时,要及时跟你做过的项目结合起来,这样在面试时就知道怎么聊了,由项目讲到知识点,由一个知识点串联到另一个知识点。复习到一定阶段,可以尝试着去把这些东西串联起来,由点及面,形成知识体系

    对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们

    技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面

    Android 架构师之路还很漫长,与君共勉

    相关文章

      网友评论

        本文标题:Android Jetpack系列(八):WorkManager

        本文链接:https://www.haomeiwen.com/subject/zrvywrtx.html