WorkManager基本使用及源码分析(一) - 使用篇

2021年02月06日 99 字 Jetpack


目录


ZERO

Q: 什么是WorkManager?为什么要使用它?

WorkManager 是一个 API,可供您轻松调度那些即使在退出应用或重启设备后仍应运行的可延期异步任务。WorkManager API 是一个适合用来替换先前的 Android 后台调度 API(包括 FirebaseJobDispatcher、GcmNetworkManager 和 JobScheduler)的组件,我们也建议您这样做。WorkManager 在其现代、一致的 API 中整合了其前身的功能,该 API 支持 API 级别 14,在开发时即考虑到了对电池续航的影响。

Q: 一定要使用WorkManager吗?

如果您的应用以 Android 10(API 级别 29)或更高版本为目标平台,那么您对 FirebaseJobDispatcher 和 GcmNetworkManager API 的调用在搭载 Android Marshmallow (6.0) 及更高版本的设备上将无法正常工作。

由上一问题可知,WorkManager是对 FirebaseJobDispatcher 和 GcmNetworkManager API 的替换,在一定情境下,显然更推荐您使用WorkManager。


本章主要内容为WorkManager基本使用及源码分析,涉及WorkManager入门使用、源码分析两个部分,可根据个人需要选择部分内容阅读。

本章所述WorkManager相关内容均基于WorkManager:2.4.0;

本章涉及代码内容均使用Java语言编写,Java版本1.8;

本章演示项目地址:https://github.com/TinloneX

参考官方文档请点击这里

1. 使用篇

WorkManager is a library used to enqueue deferrable work that is guaranteed to execute sometime after its Constraints are met. WorkManager allows observation of work status and the ability to create complex chains of work.

WorkManager是一个用于将可延迟的工作放入队列的库,这些工作保证在满足其约束后的某个时间内执行。WorkManager允许观察工作状态,并能够创建复杂的工作链。

注意:WorkManager 需要 compileSdk 版本 28 或更高版本。

1.1 声明依赖项

如需添加 WorkManager 的依赖项,您必须将 Google Maven 代码库添加到项目中:

1
2
3
4
5
6
7
// 项目根目录下build.gradle
allprojects {
repositories {
google()
jcenter()
}
}

在应用或模块的 build.gradle 文件中添加所需工件的依赖项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app/build.gradle
dependencies {
def work_version = "2.4.0"

// (Java only) 单纯使用java可引用此依赖
implementation "androidx.work:work-runtime:$work_version"

// Kotlin + coroutines 使用kotlin+协程可引用此依赖
implementation "androidx.work:work-runtime-ktx:$work_version"

// optional - RxJava2 support 意图支持RxJava2的WorkManager可引用此依赖
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"
}

注意:

  • 以上依赖均只需引用一个即可,此文档及代码均使用androidx.work:work-runtime:2.4.0
  • 特别的,①

    androidx.work:work-gcm:2.2.0 是一个新的 Maven 工件,当 Google Play 服务可用于 API 级别 22 或更低级别时,该工件支持将 GCMNetworkManager 用作调度程序。这是一个可选的依赖项,有助于在较旧的 API 版本上进行更稳定且性能更高的后台处理工作。如果您的应用使用 Google Play 服务,请将此依赖项添加到 gradle 文件,以自动获得 GCMNetworkManager 支持。如果 Play 服务不可用,WorkManager 将继续在旧款设备上回退到 AlarmManager。

下面我们将正式进入WorkManager使用篇

1.2 该如何使用

在讲解任务类型及其一般使用前,我们先了解WorkManager工作的流程:

  • 谁来做 定义一个负责工作的Worker
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class SimpleWorker extends Worker {
    @NonNull
    @Override
    public Result doWork() {
    // return Result.failure(); // 执行失败
    // return Result.retry(); // 重试
    // return Result.success(); // 执行成功
    return null; // 执行结束?
    }
    }
    Worker是WorkManager最终实现任务的“工人”,它不用管会在什么实际执行任务,被安排怎样执行任务,只管埋头处理任务doWork并根据任务执行情况反馈任务结果return Result

Worker执行结果Result:表示任务状态,他一般有failureretrysuccess三个状态,且failuresuccess可以携带数据Data,此处仅做了解,传递数据中将做具体说明。

  • 怎么做 定义制定工作方案的WorkRequest
  1. 非重复性工作: 工人应该一次性把工作做掉
    1
    OneTimeWorkRequest workRequest1 = OneTimeWorkRequest.from(SimpleWorker.class);
  2. 周期任务: 工人应该每隔多久做一次工作
1
2
PeriodicWorkRequest request = new PeriodicWorkRequest
.Builder(SimpleWorker.class, 16, TimeUnit.MINUTES).build();

注意: 关于周期任务有一个霸王条款:

注意:可以定义的最短重复间隔是 15 分钟(与 JobScheduler API 相同)。

如果您的工作的性质致使其对运行时间敏感,您可以将 PeriodicWorkRequest 配置为在每个时间间隔的灵活时间段内运行。

点此获取“灵活的运行间隔”说明

  • 谁来管 工作方案谁来审批谁来管理WorkManager
1
2
WorkManager.getInstance(myContext).enqueue(myWorkRequest);

自此,通过Worker + WorkRequest + WorkManager就可以完成一次最简工作的调用流程,其中Worker执行工作,WorkRequest封装工作,WorkManager管理工作

1.3 WorkRequest类型

WorkRequest 目前有两个子类,分别为单次执行任务 OneTimeWorkRequest和周期执行任务 PeriodicWorkRequest. 他们的主要区别是任务执行方案不同导致的状态变化不同。

简单单次执行任务 OneTimeWorkRequest 工作状态图

OneTimeWork_STATE

对于 one-time 工作请求,工作的初始状态为 ENQUEUED。

在 ENQUEUED 状态下,您的工作会在满足其 Constraints 和初始延迟计时要求后立即运行。接下来,该工作会转为 RUNNING 状态,然后可能会根据工作的结果转为 SUCCEEDED、FAILED 状态;或者,如果结果是 retry,它可能会回到 ENQUEUED 状态。在此过程中,随时都可以取消工作,取消后工作将进入 CANCELLED 状态。

周期执行任务 PeriodicWorkRequest 工作状态图

PeriodicWork_STATE

周期性工作的初始状态为 ENQUEUED,正常执行情况下,周期性工作的状态在ENQUEUED ↔RUNNING之间交替,知道任务被取消时,状态为CANCELLED。

成功和失败状态仅适用于一次性工作和链式工作。定期工作只有一个终止状态 CANCELLED。这是因为定期工作永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。

若工作状态图无法展示,参见Android-Developers/WorkManager

1.4 一般操作

1.2中,描述了最简工作的编写,那么除了这些基本的谁来做怎么做谁来管外,WorkManager还有哪些可以操作的点呢?

1.4.1 任务监听 (怎么关注任务进度)

上面1.3中介绍了的状态,我们该如何监听这些状态呢?

方式1 :

注:本文及案例未详尽的实践此方式, 此小节内容均来自官方文档

在将工作加入队列后,您可以随时按其 name、id 或与其关联的 tag 在 WorkManager 中进行查询,以检查其状态

1
2
3
4
5
6
7
8
9
10
// by id
workManager.getWorkInfoById(syncWorker.id); // ListenableFuture<WorkInfo>
// by name
workManager.getWorkInfosForUniqueWork("sync"); // ListenableFuture<List<WorkInfo>>
// by tag
workManager.getWorkInfosByTag("syncTag"); // ListenableFuture<List<WorkInfo>>

// 此处尝试使用此方式监听,可监听到部分状态
WorkManager.getInstance(this).getWorkInfoById(workRequest.getId()).addListener(() -> Utils.log(future), Runnable::run);

该查询会返回 WorkInfo 对象的 ListenableFuture,该值包含工作的 id、其标记、其当前的 State 以及通过 Result.success(outputData) 设置的任何输出数据。

方式2:

利用每个方法的 LiveData 变种,您可以通过注册监听器来观察 WorkInfo 的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(workRequest.getId())
.observe(lifecycleOwner, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
// workInfo.getState() 可获取任务状态
Utils.log(workInfo.getState());
// workInfo.getOutputData() 可获取任务传递的数据(详见1.4.2)
if (workInfo.getState().isFinished()) {
Utils.log(workInfo.getOutputData());
}
}
});

复杂的工作查询:

官方文档

WorkManager 2.4.0 及更高版本支持使用 WorkQuery 对象对已加入队列的作业进行复杂查询。WorkQuery 支持按工作的标记、状态和唯一工作名称的组合进行查询。

以下示例说明了如何查找带有“syncTag”标记、处于 FAILED 或 CANCELLED 状态,且唯一工作名称为“preProcess”或“sync”的所有工作。

1
2
3
4
5
6
7
8
9
WorkQuery workQuery = WorkQuery.Builder
.fromTags(Arrays.asList("syncTag"))
.addStates(Arrays.asList(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
.addUniqueWorkNames(Arrays.asList("preProcess", "sync")
)
.build();

ListenableFuture<List<WorkInfo>> workInfos = workManager.getWorkInfos(workQuery);

注:经实践,此方法获取的是workManager.getWorkInfos(workQuery)调用时任务的状态,若想使用此方法持续观察任务状态,建议使用以下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 此处应将Utils.executor()视作 ThreadPoolExecutor
Utils.executor().execute(new Runnable() {
@Override
public void run() {
while (true) {
ListenableFuture<List<WorkInfo>> workInfos = WorkManager.getInstance(MainActivity.this)
.getWorkInfos(workQuery);
try {
List<WorkInfo> infos = workInfos.get();
Utils.log(infos);
for (WorkInfo info : infos) {
if (info.getState() == WorkInfo.State.SUCCEEDED ||
info.getState() == WorkInfo.State.CANCELLED) {
return;
}
}
Thread.sleep(1000L);
} catch (Exception e) {
Utils.log(e);
}
}
}
});

附:如需了解如何观察工作器的中间进度,可点击此处查看

特别的,我们可以借助AndroidStudio看板观察WorkManager状态。

  • AndroidStudio白狐之前的版本可使用DataBase Inspector 观察 androidx.work.workdb数据库中WorkSpec表获取WorkRequest信息;
  • AndroidStudio白狐版则可直接使用WorkManager Inspector查看WorkRequest信息。

1.4.2 传递数据

在一些业务场景下,我们需要向任务中传递数据,可以使用androidx.work.Data向WorkRequest中添加数据。

1
2
3
4
5
6
7
8
// 创建需传递的数据
Data data = new Data.Builder().putString("message", "MainActivity").build();
// 单次执行任务
OneTimeWorkRequest request1 = new OneTimeWorkRequest.Builder(Task4DataWorker.class)
// 向WorkRequest中添加数据
.setInputData(data).build();
// 将任务加入队列
WorkManager.getInstance(this).enqueue(request1);

需要注意的是,由Data源码可知,此处传递数据仅支持基础数据类型及其封装、String以及上述类型的数组:

Puts an input key-value pair into the Builder. Valid types are: Boolean, Integer, Long, Float, Double, String, and array versions of each of those types. Invalid types throw an {@link IllegalArgumentException}.

当然的,有没有想使用反射向Data中的mValues注入其他类型呢?Map mValues = new HashMap<>();, 期待你的尝试!

有传递数据自然有获取数据,我们可以Worker中通过getInputData()获取
传入的Data对象,通过Data获取传入的值,也可以通过Data和Result将值传递给观察者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Task4DataWorker extends Worker {

public Task4DataWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}

@SuppressLint("RestrictedApi")
@NonNull
@Override
public Result doWork() {
String message = getInputData().getString("message");
Data myMsg = new Data.Builder().putString("message", "message from Worker").build();
return new Result.Success(myMsg);
}
}

而从Worker中传出的Data则可在WorkRequest的工作状态WorkInfo中取得:

1
2
3
4
5
6
7
8
9
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(workRequest.getId())
.observe(context, workInfo -> {
if (workInfo!=null){
if (workInfo.getState().isFinished()) {
String message = workInfo.getOutputData().getString("message");
}
}
});

1.4.3 多任务串联

除一般的单一任务的场景外,我们面对的业务往往也要处理多个任务的场景,此时我们可以串联多个任务,也可以将多个任务编组成任务链去使用:

1
2
3
4
5
6
7
8
9
10
11
WorkManager.getInstance(this).beginWith(workRequest2)
.then(workRequest3)
.then(workRequest4)
.enqueue();
// or
WorkContinuation continuation = WorkManager.getInstance(this)
.beginWith(workRequest2)
.then(workRequest3)
.then(workRequest4);

continuation.then(workRequest).enqueue();

特别的,如需了解任务链的高级用法及合并器相关内容,请点击此处查看官方文档,其中关于链接和工作状态的说明图文并茂,更易于理解。

1.4.4 唯一任务

唯一工作是一个很实用的概念,可确保同一时刻只有一个具有特定名称的工作实例。与 ID 不同的是,唯一名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,唯一名称仅与一个工作实例相关联。

唯一工作既可用于一次性工作,也可用于定期工作。您可以通过调用以下方法之一创建唯一工作序列,具体取决于您是调度重复工作还是一次性工作。WorkManager.enqueueUniqueWork()(用于一次性工作)
WorkManager.enqueueUniquePeriodicWork()(用于定期工作)

这两种方法都接受 3 个参数:

uniqueWorkName - 用于唯一标识工作请求的 String。
existingWorkPolicy - 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作。如需了解详情,请参阅冲突解决政策。
work - 要调度的 WorkRequest。
1
2
3
4
5
6
7
8
9
10
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(SimpleWorker2.class).build();
OneTimeWorkRequest workRequest2 = new OneTimeWorkRequest.Builder(SimpleWorker2.class).build();

WorkManager.getInstance(this).beginUniqueWork("SimpleWorker",
ExistingWorkPolicy.REPLACE, workRequest)
// ExistingWorkPolicy.APPEND, workRequest)
// ExistingWorkPolicy.KEEP, workRequest)
.then(workRequest2)
// .then(workRequest)
.enqueue();

此处定义的唯一,仅在任务正在执行且出现相同uniqueWorkName名称时,existingWorkPolicy才生效,无法影响已结束的同名任务(此同名仅与uniqueWorkName有关)。
以定义两个相同uniqueWorkName的WorkRequest为例,来观察existingWorkPolicy值的作用及影响:

1
2
3
4
5
6
7
8
9
10
11
12
// 若发现名为“SimpleWorker”的任务,则使用此链替换目标后续操作
// 任务链 A
WorkManager.getInstance(this).beginUniqueWork("SimpleWorker",
ExistingWorkPolicy.REPLACE, workRequest)
.then(request3)
.enqueue();
// 若发现名为“SimpleWorker”的任务,则保留目标的原操作,此链不执行
// 任务链 B
WorkManager.getInstance(this).beginUniqueWork("SimpleWorker",
ExistingWorkPolicy.KEEP, workRequest5)
.then(request2)
.enqueue();

上述两个任务链,若先执行A再执行B,此时效果为:

workUnique2 start   // 任务链A触发
workUnique2 end     // 任务链A触发完毕
doWork start: SimpleWorker  // 任务链A耗时任务workRequest开始执行
workUnique start    // 任务链B触发
workUnique end      // 任务链B触发完毕
doWork end: SimpleWorker // 任务链A耗时任务workRequest执行完毕
doWork: SimpleWorker3   // 任务链A执行后续任务request3
任务链B并未执行

上述两个任务链,若先执行B再执行A,此时效果为:

workUnique start    // 任务链B触发
workUnique end      // 任务链B触发完毕
doWork start: SimpleWorker5 // 任务链B耗时任务workRequest5开始执行
workUnique2 start   // 任务链A触发
workUnique2 end     // 任务链A触发完毕
doWork start: SimpleWorker  // 任务链A耗时任务workRequest开始执行
doWork end: SimpleWorker5   // 任务链B耗时任务workRequest5执行完毕
doWork end: SimpleWorker    // 任务链A耗时任务workRequest执行完毕
doWork: SimpleWorker3       // 任务链A执行后续任务request3
任务链B耗时任务被执行但后续操作被同名任务链A覆盖

同理,我们将任务链A的冲突方案分别定义为ExistingWorkPolicy.APPENDExistingWorkPolicy.APPEND_OR_REPLACE,观察执行情况,得出如下结论:

  • ExistingWorkPolicy.REPLACE 会替换正在执行的同名任务重新执行,并使用自己的任务链覆盖原任务链;
  • ExistingWorkPolicy.KEEP 会保持原任务链执行,自己不执行;
  • ExistingWorkPolicy.APPEND 会等待原任务链同名任务及后续任务执行完毕,才执行自己的任务链;
  • ExistingWorkPolicy.APPEND_OR_REPLACE 若原任务链及新任务链未被取消或中断,则
    与APPEND性质一致, 其他情况:

    If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as the child of all the leaves of that work sequence. Otherwise, insert the newly-specified work as the start of a new sequence. Note: If there are failed or cancelled prerequisites, these prerequisites are dropped and the newly-specified work is the start of a new sequence.如果存在具有相同唯一名称的未决(未完成)工作,则将新指定的工作附加为该工作序列的所有叶节点的子节点。否则,插入新指定的工作作为新序列的开始。注意:如果有失败或取消的先决条件,这些先决条件将被删除,而新指定的工作将是一个新序列的开始。

1.4.5 任务约束

参见文档

Constraints可确保将工作延迟到满足最佳条件时运行。以下约束适用于 WorkManager。

NetworkType     约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。
BatteryNotLow     如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。
RequiresCharging     如果设置为 true,那么工作只能在设备充电时运行。
DeviceIdle     如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。如果您要运行批量操作,否则可能会降低用户设备上正在积极运行的其他应用的性能,建议您使用此约束。
StorageNotLow     如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行

注意:
实际测试中,使用Android 6.0~11.0等一系列版本的模拟器测试发现

  • 9.0以下版本手机,不论应用是否在前台,进程是否存活,只要满足约束条件任务就会尽快执行;
  • 9.0及以上版本手机,应用进程DEAD状态,在符合约束条件时任务也不一定会尽快执行,甚至即使进入App也不一定能立马得到执行,除非有新的WorkRequest入队。

1.4.6 任务延时

参见文档

如果工作没有约束,或者当工作加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动。

1
2
3
4
WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
.setInitialDelay(10, TimeUnit.MINUTES)
.build();

注意:执行工作器的确切时间还取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供可能的最佳行为。
定期工作只有首次运行时会延迟

1.4.7 重试和退避政策

参见文档

若Worker返回结果Result.retry()时,触发重试退避政策,即下次调度Worker应在多长时间以后,支持设置退避时间基数和基数递增方式,递增方式目前支持线性LINEAR和指数EXPONENTIAL

1
2
3
4
5
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(RetryWorker.class)
.setBackoffCriteria(BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build();

注意:退避延迟时间不精确,在两次重试之间可能会有几秒钟的差异,但绝不会低于配置中指定的初始退避延迟时间。

退避时间在满足执行条件情况下也并不会完全精确,若不满足执行条件则会等待其条件满足:

1
2
3
4
5
6
7
8
9
10
11
12
// 无约束
间隔时间(ms): 10109
间隔时间(ms): 20094
间隔时间(ms): 30093
...
// 约束:计费(量)网络环境下
间隔时间(ms): 10121
间隔时间(ms): 20143
间隔时间(ms): 30103
// 关闭wifi喝个茶再连接wifi
间隔时间(ms): 146521
间隔时间(ms): 50132

1.4.8 标记工作

参见文档

你可以为WorkRequest添加tag,从而使得你可以通过WorkManager.getWorkInfosByTag(String)获取WorkRequest的工作状态WorkInfo,你也可以直接通过WorkManager.cancelAllWorkByTag(String)取消对应标记的所有WorkRequest.

由WorkRequest中关于tag的定义Set mTags可知,你可以为WorkRequest定义多个标记,当然的,你也可以为多个WorkRequest定义同一个标记用以统一管理。

1
2
3
4
5
6
7
8
WorkRequest request =
new OneTimeWorkRequest.Builder(SimpleWorker.class)
.addTag("TAG")
.build();
// 根据tag取消任务
WorkManager.getInstance(this).cancelAllWorkByTag("TAG");
// 根据tag查找任务状态
WorkManager.getInstance(this).getWorkInfosByTag("TAG");

1.4.9 取消工作

1
2
3
4
5
6
// by id
workManager.cancelWorkById(syncWorker.id);
// by name
workManager.cancelUniqueWork("sync");
// by tag
workManager.cancelAllWorkByTag("syncTag");

点此了解“停止正在运行的工作器”的相关内容

1.5 注意事项

这里我们总结一下使用WorkManager的一些小Tips。

  • PeriodicWorkRequest周期任务可以定义的最短重复间隔是 15 分钟(与 JobScheduler API 相同)
  • 延迟工作:执行工作器的确切时间还取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供可能的最佳行为。
  • 退避延迟时间不精确,在两次重试之间可能会有几秒钟的差异,但绝不会低于配置中指定的初始退避延迟时间。
  • cancelAllWorkByTag(String) 会取消具有给定标记的所有工作。
  • WorkRequest保证一定执行,但不保证一定在什么时间执行。

引用文献:

  1. WorkManager官方文档

源码篇正在奋力码字中。。。