Android任务调度管理,使用WorkManager进行任务调度

  • Post author:
  • Post category:其他


WorkManager API可以很容易的指定一个可延迟的异步任务何时运行。这些api允许你创建一个任务并将其交给WorkManager以立即或在适当的时间运行。比如说,一个APP可能需要时不时的从服务器下载新的资源,使用这些类,我们可以创建一个任务,为它选择合适的运行环境(比如“只有当设备在充电和在线的时候”),然后交给WorkManager使其能在满足条件时运行。即使APP强制退出或设备重新启动,任务仍然保证能够运行。

WorkManager用于那些需要保证即使APP退出了系统依然可以运行的任务,比如将应用数据上传到服务器。不要用于如果APP被杀进程,可以安全终止的后台任务;对于这种情况,建议使用ThreadPools。

WorkManager会根据设备的系统版本和APP的状态等因素选择适当的方式来运行任务。如果APP正在运行,WorkManager会在APP进程中起一个新线程来运行任务;如果APP没有运行,WorkManager会选择一个合适的方式来调度后台任务–根据系统级别和APP状态,WorkManager可能会使用JobScheduler,FireBase JobDispatcher或者AlarmManager。我们不需要编写逻辑代码来确定设备具备什么功能选择什么样的API,WorkManager会自动选择最佳方案。

此外,WorkManager还提供了几个高级特性。例如,你可以设置一个任务链,当一个任务结束之后,WorkManager会自动执行下一个在链中排队的任务。我们还可以通过观察任务的LiveData来获取它的状态和返回值,从而可以设置一个显示任务状态的UI。

本文概述了最重要的WorkManager特性。当然,还有好多可用的特性,若想了解全部的细节,可以查看WorkManager reference documentation

关于如何将WorkManager库导入到Android项目中,可以查看Adding Components to your Project这篇文章

类和概念

WorkManager API使用几个不同的类。在某些情况下,您需要对其中一个API类进行子类化。

比较重要的类有:

Worker

指定需要执行的任务。WorkManager api包含一个抽象的Worker类。我们需要继承并实现这个类

WorkRequest

表示一个独立的任务。一个WorkRequest对象需要至少指定一个执行该任务的Worker类。当然我们也可以添加更多的细节,比如指定任务应该运行的环境等。每一个WorkRequest都有一个自动生成唯一ID,我们可以使用这个ID来执行诸如取消排队任务或者获取任务状态等操作。WorkRequest是一个抽象类,我们可以使用系统提供的子类-OneTimeWorkRequest 和 PeriodicWorkRequest

WorkRequest.Builder:

创建WorkRequest对象的帮助类。同样,我们也需要用系统提供的子类:OneTimeWorkRequest.Builder 或者 PeriodicWorkRequest.Builder。

Constraints

指定任务运行的限制条件(例如,“仅当连接到网络时”)。使用Constraint.Builder来创建Constraints,并在创建WorkRequest之前把Constraints传给WorkRequest.Builder。

WorkManager

对工作请求进行管理。我们需要把WorkRequest对象传给WorkManager以便将任务编入队列。WorkManager以这样的方式调度任务,以分散系统资源的负载,同时满足我们指定的约束条件。

WorkStatus

包含特定任务的信息。WorkManager为每个WorkRequest对象提供一个LiveData。LiveData持有一个WorkStatus对象;通过观察这个LiveData,我们可以确定任务的当前状态,并在任务完成后获得返回值。

典型的工作流程

假设我们正在编写一个图片库的APP,该APP需要定期压缩存储的图像。我们使用WorkManager来调度图像压缩的任务。在这种情况下,我们并不关心压缩任务发生的时间,我们只需要设置一个任务,然后其他都不关心了。

首先,需要定义一个Worker类并重写doWork()方法。worker类指定了如何执行操作,但是没有任何关于任务应该何时运行的信息。

public class CompressWorker extends Worker {

@Override

public Worker.WorkerResult doWork() {

// Do the work here–in this case, compress the stored images.

// In this example no parameters are passed; the task is

// assumed to be “compress the whole library.”

myCompress();

// Indicate success or failure with your return value:

return WorkerResult.SUCCESS;

// (Returning RETRY tells WorkManager to try this task again

// later; FAILURE says not to try again.)

}

}

接下来,基于Worker创建一个OneTimeWorkRequest对象,然后使用WorkManager将任务放入队列中:

OneTimeWorkRequest compressionWork =

new OneTimeWorkRequest.Builder(CompressWorker.class)

.build();

WorkManager.getInstance().enqueue(compressionWork);

WorkManager选择适当的时间来运行任务,平衡诸如系统上的负载、设备是否正在充电等方面的考虑。在大多数情况下,如果不指定任何约束,WorkManager会立即运行任务。如果您需要获取任务状态,您可以通过获取适当的LiveData来获得一个WorkStatus对象。例如,如果您想检查任务是否完成,可以使用如下代码:

WorkManager.getInstance().getStatusById(compressionWork.getId())

.observe(lifecycleOwner, workStatus -> {

// Do something with the status

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

// …

}

});

任务约束

我们可以通过约束条件来指定任务何时运行。例如,我们可能希望指定该任务只应在设备空闲并连接电源时运行。在这种情况下,我们需要去创建OneTimeWorkRequest.Builder对象,然后使用这个Builder去创建OneTimeWorkRequest:

// Create a Constraints that defines when the task should run

Constraints myConstraints = new Constraints.Builder()

.setRequiresDeviceIdle(true)

.setRequiresCharging(true)

// Many other constraints are available, see the

// Constraints.Builder reference

.build();

// …then create a OneTimeWorkRequest that uses those constraints

OneTimeWorkRequest compressionWork =

new OneTimeWorkRequest.Builder(CompressWorker.class)

.setConstraints(myConstraints)

.build();

然后把这个OneTimeWorkRequest对象传给WorkManager.enqueue(),WorkManager在找到运行任务的时间时会考虑这个约束的。

取消任务

UUID compressionWorkId = compressionWork.getId();

WorkManager.getInstance().cancelWorkById(compressionWorkId);

WorkManager会尽最大努力的取消任务,但这并不靠谱——当我们试图取消任务时,任务可能已经在运行中或者已经完成了。WorkManager还提供了方法来取消唯一工作序列中的所有任务,或使用指定标记的所有任务,当然同样不靠谱。

高级特性

WorkManager API的核心功能能够创建简单的、即发即忘的任务。除此之外,API还提供了一些高级特性,来设置更详细的请求。

循环任务

我们都会碰到需要重复执行的任务。比如,一个照片管理应用不会只压缩一次图片,更有可能的是,它会时不时的检查一下共享的照片,看看是否有新的或者修改过的图片需要压缩。我们可以选择一个重复执行的任务来压缩图片,当然,也可以启动一个新任务。

new PeriodicWorkRequest.Builder photoCheckBuilder =

new PeriodicWorkRequest.Builder(PhotoCheckWorker.class, 12,

TimeUnit.HOURS);

// …if you want, you can apply constraints to the builder here…

// Create the actual work object:

PeriodicWorkRequest photoCheckWork = photoCheckBuilder.build();

// Then enqueue the recurring task:

WorkManager.getInstance().enqueue(photoCheckWork);

WorkManager会试图在请求的时间间隔运行该任务,这取决于我们强加的约束和它的其他需求了。

任务链

应用程序可能需要按特定的顺序运行多个任务。WorkManager支持创建一个工作序列,该序列指定多个任务以及它们应该运行的顺序。

WorkManager.getInstance()

.beginWith(workA)

// Note: WorkManager.beginWith() returns a

// WorkContinuation object; the following calls are

// to WorkContinuation methods

.then(workB) // FYI, then() returns a new WorkContinuation instance

.then(workC)

.enqueue();

WorkManager根据每个任务指定的约束,按所请求的顺序执行任务。如果有任意任务返回“Worker.WorkerResult.FAILURE”,则整个工作序列都将结束。

我们还可以将多个OneTimeWorkRequest对象传递给beginWith()和then()调用。如果我们将多个OneTimeWorkRequest对象传递给单个方法调用,那么WorkManager在运行其余的序列之前就会运行所有这些任务(并行)。比如:

WorkManager.getInstance()

// First, run all the A tasks (in parallel):

.beginWith(workA1, workA2, workA3)

// …when all A tasks are finished, run the single B task:

.then(workB)

// …then run the C tasks (in any order):

.then(workC1, workC2)

.enqueue();

通过将多个链与WorkContinuation.combine()方法连接起来,可以创建更复杂的序列。

打个比方,假设我们现在想运行这样一个工作序列

e6394d68931b1f9e27e1ea3379b353ca.png

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在workB之前运行workA。它在workD之前运行workc。在WordB和workD结束后,WorkManager运行workE。

注意看黑板!

虽然WorkManager按顺序运行每个子链,但是chain1中的任务与chain2中的任务顺序是不相关的。

例如,workB可能在workC之前或之后运行,或者它们可能同时运行。

唯一可以保证的是,每个子链中的任务将按顺序运行;也就是说,workB会等到workA完成后才开始。

WorkContinuation还有许多变体,为一些特定情况提供了现成的方法,具体可以参考WorkContinuation

唯一的工作序列

用beginUniqueWork()代替beginWith()就可以创建一个唯一的工作序列。每一个唯一工作序列都有一个名字;WorkManager每次只允许有一个使用该名称的工作序列。当我们创建一个新的惟一的工作序列时,如果已经有一个同名的未完成序列,可以指定WorkManager应该做什么:

取消原有的序列并用新的来代替它

保留原有的序列并忽略新的请求

把新的工作序列拼到原有序列的后边,当原有的序列的最后一个任务执行完之后,接着执行新的序列的第一个任务。

如果有一个不应该多次排队的任务,那么唯一的工作序列就很有用了。例如,如果应用程序需要将其数据同步到网络中,我们可以将一个名为“sync”的序列编入队列,并指定如果已经有一个具有该名称的序列,则应该忽略新任务。如果需要逐步构建一个长长的任务链,那么唯一的工作序列也很有用。例如,一个照片编辑应用程序可以让用户撤消一系列的操作。每个撤销操作都可能需要一段时间,但是它们必须按照正确的顺序执行。在这种情况下,应用程序可以创建一个“撤消”链,并根据需要将每个“撤消”操作附加到链上。

任务标签

可以通过为WorkRequest对象分配一个标签来对任务进行分组。

OneTimeWorkRequest cacheCleanupTask =

new OneTimeWorkRequest.Builder(MyCacheCleanupWorker.class)

.setConstraints(myConstraints)

.addTag(“cleanup”)

.build();

WorkManager类提供了一些实用方法,可以使用特定的标记对所有任务进行操作。例如,WorkManager.cancelAllWorkByTag()用一个特定的标记取消所有任务,而WorkManager.getStatusesByTag()返回一个列表,其中列出了所有带有该标记的任务的所有工作状态。

入参和返回值

为了更好的灵活性,我们还可以向任务传递参数并让任务返回结果,传递的值和返回的值是键值对形式。要将一个参数传递给一个任务,需要在创建WorkRequest对象之前调用WorkRequest.Builder.setInputData()方法,该方法接收一个Data.Builder创建的Data对象。Worker类可以通过调用Worker.getInputData()访问这些参数。任务需要调用Worker.setOutputData()来输出返回值,同样接收一个Data对象,我们可以通过观察任务的LiveData来获得返回值。

假设我们有一个执行耗时操作的Worker:

// Define the Worker class:

public class MathWorker extends Worker {

// Define the parameter keys:

public static final String KEY_X_ARG = “X”;

public static final String KEY_Y_ARG = “Y”;

public static final String KEY_Z_ARG = “Z”;

// …and the result key:

public static final String KEY_RESULT = “result”;

@Override

public Worker.WorkerResult doWork() {

// Fetch the arguments (and specify default values):

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 the math…

int result = myCrazyMathFunction(x, y, z);

//…set the output, and we’re done!

Data output = new Data.Builder()

.putInt(KEY_RESULT, result)

.build();

setOutputData(output);

return WorkerResult.SUCCESS;

}

}

创建一个任务并传递参数:

// Create the Data object:

Data myData = new Data.Builder()

// We need to pass three integers: X, Y, and Z

.putInt(KEY_X_ARG, 42)

.putInt(KEY_Y_ARG, 421)

.putInt(KEY_Z_ARG, 8675309)

// … and build the actual Data object:

.build();

// …then create and enqueue a OneTimeWorkRequest that uses those arguments

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

.setInputData(myData)

.build();

WorkManager.getInstance().enqueue(mathWork);

通过WorkStatus获取返回值:

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

.observe(lifecycleOwner, status -> {

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

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

myDefaultValue));

// … do something with the result …

}

});

如果是一个任务链,那么一个任务的返回值可以作为下一个任务的参数。如果是一个简单的任务链,一个OneTimeWorkRequest后面跟着另一个OneTimeWorkRequest,第一个任务通过调用setOutputData()返回结果,下一个任务通过调用getInputData()来获取结果。如果是一个更复杂的任务链,比如说,有几个任务都将返回值传递给下一个任务,我们可以通过OneTimeWorkRequest.Builder上定义一个InputMerger,指定如果不同的任务返回一个具有相同键的输出时应该怎么做。