Android Support WorkManager使用详解
使用WorkManager调度任务
WorkManager是一个库, 用以将工作入队, 当该工作的限制条件得到满足之后, WorkManager保证它的执行. WorkManager允许观测工作的状态, 并且拥有能力创建复杂的工作链.
WorkManager使用基础的作业分发服务, 仅当如下条件可用时:
- 当API 23+时, 使用JobScheduler;
- 当API 14~22时, 如果应用使用了Firebase JobDispatcher和其它的Firebase依赖时, 使用Firebase JobDispatcher; 否则的话, 则使用自定义的AlarmManager + BroadcastReceiver实现.
所有的工作必须有相应的Worker来实现计算. 工作在后台线程执行. WorkManager支持两种类型的工作: OneTimeWorkRequest和PeriodicWorkRequest.
默认情况下, WorkManager在后台线程执行它的操作. 如果你已经在后台线程运行且需要同步调用WorkManager, 使用synchronous()方法访问这些方法.
WorkManager API可以轻易的指定可暂缓的异步任务, 以及这些任务何时运行. 这些API能够让你创建一个任务并把该任务推给WorkManager即刻或者在恰当的时刻去运行. 比如, 某个应用可能时不时地需要从网络下载最新的资源. 使用这些类, 你可以设置一个任务, 选择该任务运行时的恰当条件(例如”只有在设备充电且联网的时候”), 然后把任务推给WorkManager, 当指定的条件满足时, WorkManager便会运行该任务. 即使你的应用被强制退出或者设备进行了重启, 这些任务依然会被保障执行.
备注: 例如, 向服务器上传应用数据, 即使应用退出, 系统依然保障运行这些任务. WorkManager即是为这些任务而设计的. 而对于进程内的后台工作, 在应用的进程结束的时候, 则会被安全地终止, 这些任务并不是WorkManager设计的目的. 而这些这种情况, 推荐使用ThreadPools.
WorkManager基于设备的API和应用的状态来选择恰当的方式来执行任务. 如果WorkManager在应用正在运行的时候执行了你的某条任务, 那么WorkManager会在你的应用进程中开启新的线程来执行这条任务. 而如果你的应用没有在运行, WorkManager则会选择恰当的方式来调度后台任务-根据设备API水平和依赖库, WorkManager可能会选择JobScheduler, Firebase JobDispatcher或者AlarmManager. 你不必自己写逻辑找出设备有哪些能力以找出恰当的API; 正相反, 你只需把任务递交给WorkManager让它选择最佳选项.
此外, WorkManager还提供了些许高级特性. 比如, 你可以设置任务链; 在一些任务完成之后, WorkManager将下一个任务添加进队列以形成链. 你也可以通过观测任务的LiveData来查看它的状态和返回值. 如果你想通过展示UI来显示任务状态, 这也许会有用.
同时, WorkManager已经存在于androidx.arch包中, 但它依然依赖于Android Support Library 27.1并与Arch构件的版本有关联. 拥有AndroidX依赖的WorkManager版本将会在未来发布.
WorkManager的依赖添加方式如下:
1 dependencies { 2 def work_version = "1.0.0-alpha04" 3 4 implementation "android.arch.work:work-runtime:$work_version" // use -ktx for Kotlin 5 6 // optional - Firebase JobDispatcher support 7 implementation "android.arch.work:work-firebase:$work_version" 8 9 // optional - Test helpers 10 androidTestImplementation "android.arch.work:work-testing:$work_version" 11 }
相关类和概念
WorkManager API使用了几个不同的类. 某些情况下, 你需要继承某些类.
重要的一些类有:
- Worker: 指定了你需要执行什么样的任务. WorkManager包含了抽象类Worker. 你需要继承它并在里面执行工作.
- WorkRequest: 表示一个单独的任务. 最少, WorkRequest对象指定了哪个Worker执行该任务. 然后, 你也可以向WorkRequest添加一些细节, 指定诸如任务在什么条件下执行等这些事情. 每一个WorkRequest都拥有一个自动生成的独一无二的ID. 你可以使用该ID来做一些事情, 例如, 取消已经入队的任务, 或者, 获取任务的状态. WorkRequest是个抽象类, 使用时, 应该使用它的直接子类, OneTimeWorkReqeust或者PeriodicWorkRequest.
- WorkRequest.Builder: 帮助类, 用来创建WorkRequest对象. 再说一遍, 你需要使用它的子类, OneTimeWorkRequest.Builder或者PeriodicWorkRequest.Builder.
- Constraints: 指定任务运行时机的限制条件(例如”只有在连接到网络的时候”). 你可以使用Constraints.Builder来创建Constraints对象, 并且在创建WorkRequest对象之前, 把Constraints传递给WorkerRequest.Builder.
- WorkerManager: 入队并管理工作请求. 你把WorkRequest对象传递给WorkManager以入队任务. WorkManager以这种方式调度任务来分散系统资源的加载, 同时尊重了你指定的限制条件.
- WorkStatus: 包含了特定任务的信息. WorkManager为每一个WorkRequest对象提供了LiveData. LiveData持有WorkStatus对象; 通过观测LiveData, 你能够判定任务的当前状态, 以及在任务结束后, 获取返回值.
经典工作流
假设你正在写一个图片库应用, 该应用需要定期压缩存储的图片. 你想要使用WorkManager API来执行图片压缩. 在这种情况下, 你不必特别关注压缩在什么时候执行; 你想要设置好任务, 然后不再关注它.
首先, 你需要定义自己的Worker类, 然后覆盖doWork方法. 你的Worker类详述了操作该怎么执行, 却没有任何信息表明任务何时运行.
1 class CompressWorker : Worker() { 2 3 override fun doWork(): Result { 4 5 // Do the work here--in this case, compress the stored images. 6 // In this example no parameters are passed; the task is 7 // assumed to be "compress the whole library." 8 myCompress() 9 10 // Indicate success or failure with your return value: 11 return Result.SUCCESS 12 13 // (Returning RETRY tells WorkManager to try this task again 14 // later; FAILURE says not to try again.) 15 16 } 17 18 }
然后, 基于上述Worker, 你创建了OneTimeWorkRequest对象, 然后通过WorkManager将该任务入队.
1 val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>().build() 2 WorkManager.getInstance().enqueue(compressionWork)
WorkManager选择恰当的时刻运行该任务, 在平衡了诸如系统加载, 设备插拔等等, 这些考虑之后. 在多数情况下, 你不需要指定任务限制, WorkManager即刻运行你的任务. 如果你需要检测任务状态, 可能通过对正确的LiveData<WorkStatus>的处理, 获取WorkStatus对象. 比如, 你想知道任务是否已经完成, 你可以使用这样的代码:
1 WorkManager.getInstance().getStatusById(compressionWork.id) 2 .observe(lifecycleOwner, Observer { workStatus -> 3 // Do something with the status 4 if (workStatus != null && workStatus.state.isFinished) { 5 // ... 6 } 7 })
任务限制
如果你想, 你可以指定任务何时应该运行的限制条件. 比如, 你可以指定任务只有在设备空闲且连接充电时才可以运行. 在这种情况下, 你需要创建一个OneTimeWorkReqeust.Builder对象, 然后可以使用这个Builder创建一个真正的OneTimeWorkRequest.
1 // Create a Constraints that defines when the task should run 2 val myConstraints = Constraints.Builder() 3 .setRequiresDeviceIdle(true) 4 .setRequiresCharging(true) 5 // Many other constraints are available, see the 6 // Constraints.Builder reference 7 .build() 8 9 val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>() 10 .setConstraints(myConstraints) 11 .build()
然后, 像之前一样, 将这个OneTimeWorkRequest对象传递给WorkManager.enqueue(). WorkManager会考虑你的限制条件, 直到一个时刻可以运行你的任务.
取消任务
在一个任务入队后, 你依然可以取消它. 要取消一个任务, 你需要它的工作ID, 这个ID可以从WorkRequest对象中获取到. 比如, 下面的代码取消了之前的compressionWork请求:
1 val compressionWorkId:UUID = compressionWork.getId() 2 WorkManager.getInstance().cancelWorkById(compressionWorkId)
WorkManager会尽最大努力来取消该任务, 但这是天性不确定的–因为在你尝试取消该任务时, 它可能已经在运行或者已经结束了. WorkManager也提供了方法取消在一个单独工作序列中的所有任务, 或者拥有特定tag的所有任务, 当然, 也是尽力而为.
高级功能
WorkManager API的核心功能合使你能够创建简单且”即发即弃”的任务. 除此之外, API提供了高级功能让你能够设置更多复杂的任务.
任务重现
你也许拥有一个需要反复执行的任务. 比如, 照片管理应用不会只执行一次压缩图片. 更可能的情况是, 它会想非常频繁地检查共享的照片, 来看是否有新的或者修改过的照片需要压缩. 这个再现任务能够压缩它找到的照片, 或者, 它可以接二连三地新建”压缩该图片”任务.
要创建一个重现任务, 使用PeriodicWorkRequest.Builder类创建一个PeriodicWorkRequest对象, 然后用入队OneTimeWorkRequest对象一样的方式, 入队PeriodicWorkRequest对象. 比如, 假设我们定义了PhotoCheckWorker类来识别需要压缩的图片. 如果我们想每12小时运行一次这个清单任务, 我们需要像如下一样创建一个PeriodicWorkRequest对象:
1 val photoCheckBuilder = 2 PeriodicWorkRequestBuilder<PhotoCheckWorker>(12, TimeUnit.HOURS) 3 // ...if you want, you can apply constraints to the builder here... 4 5 // Create the actual work object: 6 val photoCheckWork = photoCheckBuilder.build() 7 // Then enqueue the recurring task: 8 WorkManager.getInstance().enqueue(photoCheckWork)
WorkManager尝试运行你的任务, 在你请求的间隔, 并且服从你设置的限制条件和其它需求.
任务入链
你的应用可能需要以特定的顺序执行几个任务. WorkManager允许你创建并入队一个工作序列, 这个工作序列指定了多个任务, 以及它们运行的顺序.
比如, 假设你的应用有三个OneTimeWorkRequest对象: workA, workB和workC. 这些任务必须以A->B->C的顺序执行. 要反三个任务入队, 使用WorkManager.beginWith(), 以第一个OneTimeWorkRequest对象workA为参数, 创建工作序列. 这个方法返回了WorkContinuation对象, 该对象定义了一个任务序列. 然后, 按顺序, 用WorkContinuation.then方法添加剩下的OneTimeWorkRequest对象. 最后用WorkContinuation.enqueue方法将该工作序列入队.
1 WorkManager.getInstance() 2 .beginWith(workA) 3 // Note: WorkManager.beginWith() returns a 4 // WorkContinuation object; the following calls are 5 // to WorkContinuation methods 6 .then(workB) // FYI, then() returns a new WorkContinuation instance 7 .then(workC) 8 .enqueue()
WorkManager用请求的顺序, 根据每一个任务特定的限制, 运行任务. 如果任何一个任务返回了Worker.Result.FAILURE, 则整个序列终止.
你也可以传递多个OneTimeWorkRequest对象给beginWith()/then()方法, 如果你对单个方法调用传递了多个OneTimeWorkRequest对象, WorkManager将会并行运行该方法里面的所有任务, 在该方法里面的所有任务都完成之后, 才会执行方法序列的余下部分. 比如:
1 WorkManager.getInstance() 2 // First, run all the A tasks (in parallel): 3 .beginWith(workA1, workA2, workA3) 4 // ...when all A tasks are finished, run the single B task: 5 .then(workB) 6 // ...then run the C tasks (in any order): 7 .then(workC1, workC2) 8 .enqueue()
你可以通过WorkContinuation.combine()方法将多个链联合在一起, 来创建更加复杂的任务链. 比如你想创建如下一个任务链:
要创建这个序列, 需要创建两个单独的序列, 然后把它们结合在一起生成第三个:
1 val chain1 = WorkManager.getInstance() 2 .beginWith(workA) 3 .then(workB) 4 val chain2 = WorkManager.getInstance() 5 .beginWith(workC) 6 .then(workD) 7 val chain3 = WorkContinuation 8 .combine(chain1, chain2) 9 .then(workE) 10 chain3.enqueue()
在这个实例下, WorkManager先运行workA, 然后运行workB; 也是先运行workC, 然后运行workD. 在workB和workD都完成之后, 再运行workE.
备注: 尽管在每一个子链中WorkManager有序运行任务, 但却无法保证chains1中的任务如何与chains2中的任务重叠. 比如上面例子中的workB可以在workD之前或者之后运行, 也可以同时运行. 唯一的承诺是每一个子链中的多个任务有序运行; 也就是, workB直到workA运行结束都会开始执行.
有大量的WorkContinuation方法的变体提供了特定场景下的速记. 比如WorkContinuation.combine(OneTimeWorkRequest, WorkContinuation…)命令WorkManager先完成所有指定的WorkContinuation链, 然后以执行完OneTimeWorkRequest结束.
独立工作序列(Unique work sequences)
通过调用beginUniqueWork()而非beginWith()来开始一个序列, 就能够创建一个独立工作序列. 每一个独立工作序列都有一个名字; 一个名字, WorkManager一次只允许开启一个工作序列. 当你创建一个新的独立工作序列时, 如果已经存在了一个未完成的同名工作序列时, 你应该详述WorkManager此时应该做什么:
- 取消已存在序列并用新的序列替代它;
- 保留已存在序列并忽略新序列;
- 将新的序列附加到已存在序列末尾, 当已存在序列最后一个任务完成后, 执行新序列中的第一个任务.
当你拥有一个任务不该入队多次时, 独立工作序列会非常有用. 比如, 你的应用需要同步数据到网络时, 你可能会入队一个名叫”sync”的独立工作序列, 并且指明如果已经存在一个”sync”序列时, 新的任务应该被忽略.
当你需要逐步构建一个长的任务链时, 独立工作序列也会非常有用. 比如照片编辑应用可能会让用户撤销一长链的操作, 每一个操作的撤销可能花费很短的时间, 但是它们必须以正确的顺序执行. 在这种情形下, 应用可以创建一个”撤销”链并将每个撤销操作按需添加到链中.
工作打标
对于每一个WorkRequest对象, 你都可以给它赋值一个tag字符串, 然后从逻辑上将任务分组. 要设置tag的话, 调用WorkRequest.Builder.addTag(), 如下:
1 val cacheCleanupTask = 2 OneTimeWorkRequestBuilder<MyCacheCleanupWorker>() 3 .setConstraints(myConstraints) 4 .addTag("cleanup") 5 .build()
类WorkManager提供了几个工具方法, 允许你用特定的tag操作所有的任务. 比如, WorkManager.cancelAllWorkByTag()使用特定的tag取消所有的任务; WorkManager.getStatusesByTag()返回特定tag标记任务的WorkStatus列表.
输入参数和返回值
为了获取更大的灵活性, 你可以给任务传递参数, 也可以使任务返回结果. 被传递和返回的值是键-值对. 要给任务传参数, 在创建WorkRequest对象之前调用WorkRequest.Builder.setInputData(). 该方法的参数是Data.Builder创建的Data对象. Worker类可以通过Worker.getInputData()方法访问传入的参数. 要输出返回值, 任务调用Worker.setOutputData(). 该方法的参数也是个Data对象. 你可能通过观测任务的LiveData<WorkStatus>获取输出.
比如, 假设你有一个Worker类执行耗时的计算. 下面是这个Worker的示例:
1 // Define the parameter keys: 2 const val KEY_X_ARG = "X" 3 const val KEY_Y_ARG = "Y" 4 const val KEY_Z_ARG = "Z" 5 6 // ...and the result key: 7 const val KEY_RESULT = "result" 8 9 // Define the Worker class: 10 class MathWorker : Worker() { 11 12 override fun doWork(): Result { 13 val x = inputData.getInt(KEY_X_ARG, 0) 14 val y = inputData.getInt(KEY_Y_ARG, 0) 15 val z = inputData.getInt(KEY_Z_ARG, 0) 16 17 // ...do the math... 18 val result = myCrazyMathFunction(x, y, z); 19 20 //...set the output, and we're done! 21 val output: Data = mapOf(KEY_RESULT to result).toWorkData() 22 setOutputData(output) 23 24 return Result.SUCCESS 25 } 26 }
要创建这个工作且传参, 代码应该如下:
1 val myData: Data = mapOf("KEY_X_ARG" to 42, 2 "KEY_Y_ARG" to 421, 3 "KEY_Z_ARG" to 8675309) 4 .toWorkData() 5 6 // ...then create and enqueue a OneTimeWorkRequest that uses those arguments 7 val mathWork = OneTimeWorkRequestBuilder<MathWorker>() 8 .setInputData(myData) 9 .build() 10 WorkManager.getInstance().enqueue(mathWork)
返回值将会在任务的WorkStatus中可用:
1 WorkManager.getInstance().getStatusById(mathWork.id) 2 .observe(this, Observer { status -> 3 if (status != null && status.state.isFinished) { 4 val myResult = status.outputData.getInt(KEY_RESULT, 5 myDefaultValue) 6 // ... do something with the result ... 7 } 8 })
如果你将任务入链, 一个任务的输出是可以作为链中下一个任务的输入的. 如果是个很简单的链, 单OneTimeWorkRequest紧接着另一个单OneTimeRequest, 第一个任务通过调用setOutputData返回的值, 在第二个任务中通过调用getInputData可以获取得到. 如果工作链比较复杂, 比如多个任务都发送输出到单个任务–你可以在OneTimeWorkRequest.Builder中定义一个InputMerger, 以指定当不同任务返回的输出有相同的键时, 应该怎么处理.