APP保活的福音?WorkManager初探
说明:本文是对WorkManager的官方文档的翻译,未做实际项目研究
官方文档地址:https://developer.android.com/topic/libraries/architecture/workmanager
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。
-
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>来获得一个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()方法连接起来,可以创建更复杂的序列。
打个比方,假设我们现在想运行这样一个工作序列
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<WorkStatus>来获得返回值。
假设我们有一个执行耗时操作的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,指定如果不同的任务返回一个具有相同键的输出时应该怎么做。