SilentKnight 阅读(7) 评论(0)

 

 

使用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, 以指定当不同任务返回的输出有相同的键时, 应该怎么处理.