Android架构之WorkManager组件
Workmanager的作用
绝大部分应用程序都有在后台执行任务的需求。根据需求的不同,Android为后台任务提供了多种解决方法,如JobScheduler、Loader、Service等。
如果这些API没有被恰当地使用,则可能会消耗大量的电量。Android在解决应用程序耗电问题上做出了各种尝试,从Doze到App Standby,通过各种方法限制和管理应用程序,以保证应用程序不会再后台过分消耗设备的电量。WorkManager为应用程序中那些不需要及时完成的任务提供了一个统一的解决方法,以便在设备电量和用户体验之间达到了一个比较好的平衡。
WorkManager的3个重要特点
针对的是不需要及时完成的任务
例如: 发送应用程序日志、同步应用程序数据、备份用户数据等,站在业务需求的角度,这些任务都不需要立即完成。如果我们自己来管理这些任务,逻辑可能会非常复杂,若API使用不恰当,可能会消耗大量电量.
保证任务一定会被执行
WorkManager能保证任务一定会被执行,即使应用程序当前不在运行中,甚至在设备重启过后,任务仍然会在适当的时刻被执行。这是因为WorkManager有自己的数据库,关于任务的所有信息和数据都保存在该数据库中。因此,只要任务交给了WorkManager,哪怕应用程序彻底退出,或者设备被重新启动,WorkManager依然能够保证完成你交给它的任务
兼容范围广
WorkManager最低能兼容API Level14, 并且不需要你的设备安装GooglePlay Services。因此,不用过于担心兼容性问题。因为API Level 14已经能够兼容几乎100%的设备.
WorkManager的兼容方法
WorkManager能依据设备的情况,选择不同的执行方案。在API Level 23以上的设备中,通过JobScheduler完成任务;在API Level 23以下的设备中,通过AlarmManager和Broadccast Receivers组合来完成任务。但无论采用哪种方案,任务最终都是交由Executor来执行。
需要注意的是, WorkManager不是一种新的工作线程,它的出现不是为了替代其他类型的工作线程。工作线程通常立即运行,并在任务执行完成后给用户反馈。而WorkManager不是即时的,它不能保证任务能立即得到执行。
WorkManager的基本使用
添加依赖
在app的build.gradle中添加WorkManager所需要的依赖
//导入WorkManager
implementation "androidx.work:work-runtime:2.3.4"
Worker类介绍
Worker类是个抽象类,我们需要继承并覆盖doWork()方法,所有需要在任务中执行的代码都在该方法中进行编写
public class UploadLogWorker extends Worker {
public UploadLogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
//耗时的任务在doWork()方法中执行
return Result.success();
}
}
doWork()方法有3种类型的返回值
- 若执行成功,则返回Result.success()
- 若执行失败,则返回Result.failure().
- 若需要重新执行,则返回Result.retry();
WorkRequest类介绍
WorkRequest是一个抽象类,它有两种实现方式:
- OneTimeWorkRequest: 一次性任务
- PeriodicWorkRequest: 周期性任务
WorkRequest进行对Work类的一种包装。我们可以通过WorkRequest来配置任务,配置任务就是告诉系统,任务何时运行及如何运行。
调度一次任务
对于无需额外配置的简单工作,我们可以使用静态方法from:
WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);
对于更复杂的任务,可以使用构建器
WorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
// 配置任务
.build();
1.设置任务触发条件
例如: 我们可以设置当设备处于充电、网络已连接,且电池电量充足的状态下,才触发任务。
Constraints constraints = new Constraints.Builder()
.setRequiresChargint(true)
.setRequiredNetWorkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build();
将任务触发条件设置到WorkRequest.
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
//设置触发条件
.setConstraints(constrains)
.build();
2.设置延迟执行任务
可以通过setInitialDelay()方法,对任务进行延后执行
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
//任务 延时10s执行
.setInitialDelay(10,TimeUnit.SECONDS)
.build();
3.设置指数退避策略
假如Worker线程的执行出现了异常(比如任务执行失败),你希望一段时间后,重试该任务。 那么你可以在Worker的doWork()方法中返回Result.retry(), 系统会有默认的指数退避策略来帮你重试任务,也可以通过setBackoffCriteria()方法来自定义指数退避策略
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
//设置指数退避算法
.setBackoffCriteria(BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build();
为任务设置Tag标签
设置tag标签后,我们就可以根据该标签追踪任务的状态,也可以用来取消任务
例如,WorkManager.cancelAllWorkByTag(String) 会取消带有特定标记的所有工作请求,WorkManager.getWorkInfosByTag(String) 会返回一个 WorkInfo 对象列表,该列表可用于确定当前工作状态。
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
//任务 延时10s执行
.addTag("UploadTag")
.build();
获取设置的标签和Id
/**
* 获取 WorkRequest对应的tag
*/
public @NonNull Set<String> getTags();
/**
* 获取 WorkRequest对应的UUID
*/
public @NonNull UUID getId();
WorkManager介绍
将任务提交给系统
将任务配置好之后,需要将其提交给系统,WorkManager.enqueue()方法用于将配置好的WorkRequest交给系统来执行
WorkManager.getInstance(this).enqueue(uploadWorkRequest)
观察任务的状态
任务在提交给系统后,可以通过WorkInfo获取任务的状态。 WorkInfo包含任务的id、tag、Worker对象传递过来的outputData,以及任务当前的状态。
有三种方式可以得到WorkInfo对象
- WorkManager.getWorkInfosByTag() 根据WorkRequest设置的Tag标签
- WorkManager.getWorkInfoById() 根据Id获取
- WorkManager.getWorkInfosForUniqueWork()
如果希望实时获知任务的状态,可以获取对应的LiveData方法
- WorkManager.getWorkInfosByTagLiveData()
- WorkManager.getWorkInfoByIdLiveData()
- WorkManager.getWorkInfosForUniqueWorkLiveData()
通过LiveData,我们便可以在任务状态发送变化时收到通知
WorkManager.getInstance(this)
.getWorkInfoByIdLiveData(uploadWorkRequest.getId())
.observe(WorkManagerActivity.this, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
}
}
});
取消任务
与观察任务类似,我们也可以根据id或tag取消某个任务,或取消所有任务
//取消所有任务
WorkManager.getInstance(this).canceAllwork();
//根据Tag取消任务
WorkManager.getInstance(this).cancelAllWorkByTag()
//根据ID取消任务
WorkManager.getInstance(this).cancelWorkById()
WorkManager与Worker之间的参数传递
WorkRequest可以通过setInputData()方法向Worker传递数据。数据的传递通过Data对象来完成。需要注意的是,Data只能用于传递一些小的基本类型的数据,且数据大小不能超过10KB
Data inputData = new Data.Builder().putString("input_data","Hello World").build();
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
.setInputData(inputData)
.build();
Woker通过getInputData()方法接收数据,并在任务完成后,向WorkManager返回数据
@NonNull
@Override
//耗时的任务在doWork()方法中执行
public Result doWork() {
//接收外界传递进行的数据
String inputData = getInputData().getString("input_data");
//任务执行完成后返回数据
Data outputData = new Data.Builder().putString("out_put_data","Task Success").build();
return Result.success(outputData);
}
WorkManager通过LiveData得到从Worker返回的数据
WorkManager.getInstance(this)
.getWorkInfoByIdLiveData(uploadWorkRequest.getId())
.observe(WorkManagerActivity.this, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
if(workInfo!=null && workInfo.getState() == WorkInfo.State.SUCCEEDED){
String outputData = workInfo.getOutputData().getString("out_put_data");
textView.setText(outputData);
}
}
});
//将任务提交给系统执行
WorkManager.getInstance(this).enqueue(uploadWorkRequest);
WorkInfo
WorkInfo 是Worker返回给我们的数据, WorkInfo包含id,tag,状态,返回值
我们可以通过WorkInfo.getState()获取当然任务执行的状态(State)
State有以下几种取值
- ENQUEUED: 任务刚入队列时的状态
- RUNNING: 任务正在执行时的状态
- SUCCEEDED: 任务执行成功时的状态
- FAILED : 任务执行失败时的状态
- BLOCKED: 工作链接时的状态
- CANCELLED:任务中途被取消时的状态
我们就可以通过State来判断当前任务的状态
// 获取到LiveData然后监听数据变化
WorkManager.getInstance().getWorkInfoByIdLiveData(request.getId()).observe(this, new Observer<WorkInfo>() {
@Override
public void onChanged(@Nullable WorkInfo workInfo) {
if (workStatus == null) {
return;
}
if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
mTextOut.setText("任务入队");
}
if (workInfo.getState() == WorkInfo.State.RUNNING) {
mTextOut.setText("任务正在执行");
}
if (workInfo.getState() == WorkInfo.State.CANCELLED) {
mTextOut.setText("任务已经被取消了");
}
if (workInfo.getState() == WorkInfo.State.FAILED ) {
mTextOut.setText("任务执行失败了");
}
if (workInfo.getState() == WorkInfo.State.SUCCEEDED ) {
Data data = workStatus.getOutputData();
mTextOut.setText("任务完成" + "-结果:" + data.getString("key_name", "null"));
}
}
});
一次性工作的状态
对于 one-time 工作请求,工作的初始状态为 ENQUEUED。
在 ENQUEUED 状态下,您的工作会在满足其 Constraints 和初始延迟计时要求后立即运行。接下来,该工作会转为 RUNNING 状态,然后可能会根据工作的结果转为 SUCCEEDED、FAILED 状态;或者,如果结果是 retry,它可能会回到 ENQUEUED 状态。在此过程中,随时都可以取消工作,取消后工作将进入 CANCELLED 状态。
SUCCEEDED、FAILED 和 CANCELLED 均表示此工作的终止状态。如果您的工作处于上述任何状态,WorkInfo.State.isFinished() 都将返回 true。
定期工作的状态
成功和失败状态仅适用于一次性工作和链式工作。定期工作只有一个终止状态 CANCELLED。这是因为定期工作永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度
WorkManager基本使用
1.编写Work
public class UploadLogWorker extends Worker {
public UploadLogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
//耗时的任务在doWork()方法中执行
public Result doWork() {
//接收外界传递进行的数据
String inputData = getInputData().getString("input_data");
//任务执行完成后返回数据
Data outputData = new Data.Builder().putString("out_put_data","Task Success").build();
return Result.success(outputData);
}
}
2.WorkManagerActivity
public class WorkManagerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_work_manager);
final TextView textView = findViewById(R.id.tv_text345);
Data inputData = new Data.Builder().putString("input_data","Hello World").build();
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
.setInputData(inputData)
.build();
WorkManager.getInstance(this)
.getWorkInfoByIdLiveData(uploadWorkRequest.getId())
.observe(WorkManagerActivity.this, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
if(workInfo!=null && workInfo.getState() == WorkInfo.State.SUCCEEDED){
String outputData = workInfo.getOutputData().getString("out_put_data");
textView.setText(outputData);
}
}
});
//将任务提交给系统执行
WorkManager.getInstance(this).enqueue(uploadWorkRequest);
}
}
3.WorkManagerActivity布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".workmanager.WorkManagerActivity"
android:gravity="center">
<TextView
android:id="@+id/tv_text345"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="小鑫"
android:textSize="30sp"/>
</LinearLayout>
调度周期性任务
前面提到过,WorkRequest有两种实现方式: OneTimeWorkRequest和PeriodicWorkRequest,分别对应一次性任务和周期性任务。一次性任务在任务成功执行后,便彻底结束。而周期性任务则会按照设定的时间定期执行。二者使用没有太大差别,需要注意的是,周期性任务的间隔时间不能少于15分钟
PeriodicWorkRequest saveRequest =
new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS)
//添加配置
.build();
作的运行时间间隔定为一小时。
Constraints 工作约束
约束可确保将工作延迟到满足最佳条件时运行。有以下约束适用于WorkManager
NetWorkType | 约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。 |
---|---|
BatteryNotLow | 如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。 |
RequiresCharging | 如果设置为 true,那么工作只能在设备充电时运行。 |
DeviceIdle | 如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。如果您要运行批量操作,否则可能会降低用户设备上正在积极运行的其他应用的性能,建议您使用此约束。 |
StorageNotLow | 如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。 |
例如,以下代码会构建了一个工作请求,该工作请求仅在用户设备正在充电且连接到 Wi-Fi 网络时才会运行:
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build();
WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
.setConstraints(constraints)
.build();
如果在工作运行时不再满足某个约束,WorkManager 将停止工作器。系统将在满足所有约束后重试工作。
管理工作
定义Worker和WorkRequest后,最后一步是将工作加入队列。将工作加入队列的最简单方法就是WorkManager enqueue()方法,然后传递要运行的WorkRequest
WorkRequest myWork = // ... OneTime or PeriodicWork
WorkManager.getInstance(requireContext()).enqueue(myWork);
在将工作加入队列时请小心谨慎,以避免重复。例如,应用可能会每 24 小时尝试将其日志上传到后端服务。如果不谨慎,即使作业只需运行一次,您最终也可能会多次将同一作业加入队列。为了实现此目标,您可以将工作调度为唯一工作。
唯一工作
可确保同一时刻只有一个具体特定名称的工作实例.与 ID 不同的是,唯一名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,唯一名称仅与一个工作实例相关联。
唯一工作既可用于一次性工作,也可用于定期工作
- WorkManager.enqueueUniqueWork()(用于一次性工作)
- WorkManager.enqueueUniquePeriodicWork()(用于定期工作)
这两种方法都接受 3 个参数:
- uniqueWorkName - 用于唯一标识工作请求的 String。
- existingWorkPolicy - 此 enum 可告知 WorkManager 如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作。
- work - 要调度的 WorkRequest。
PeriodicWorkRequest sendLogsWorkRequest = new
PeriodicWorkRequest.Builder(SendLogsWorker.class, 24, TimeUnit.HOURS)
.setConstraints(new Constraints.Builder()
.setRequiresCharging(true)
.build()
)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"sendLogs",
ExistingPeriodicWorkPolicy.KEEP,
sendLogsWorkRequest);
冲突解决政策
调度唯一工作时,您必须告知 WorkManager 在发生冲突时要执行的操作。
对于一次性工作,您需要提供一个 ExistingWorkPolicy,它支持用于处理冲突的 4 个选项。
- REPLACE:用新工作替换现有工作。
- KEEP:保留现有工作,并忽略新工作
- APPEND: 将新工作附加到现有工作的末尾。
现有工作将成为新工作的先决条件。如果现有工作变为 CANCELLED 或 FAILED 状态,新工作也会变为 CANCELLED 或 FAILED如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE。 - APPEND_OR_REPLACE:即使现有工作变为 CANCELLED 或 FAILED 状态,新工作仍会运行。
链接工作
如果有一系列的任务需要按顺序执行,那么可以利用WorkManager.beginWith().then().then()…enqueue()的方式构建任务链。例如,在上传数据之前,可能需要先对数据进行压缩。
WorkManager.getInstance(this)
.beginWith(commpressWorkRequest)
.then(uploadWorkRequest)
.enqueue();
假设除了压缩数据,还需要更新本地数据。压缩数据与更新本地数据二者没有先后顺序的区别,但与上传数据存在先后顺序
WorkManager.getInstance(this)
.beginWith(commpressWorkRequest,updateLocalWorkRequest)
.then(uploadWorkRequest)
.enqueue();
也就是说压缩数据和更新本地数据同时进行,但上传数据要等到压缩数据与更新数据完成之后才会执行。
组合任务
假设有更复杂的任务链,那么可以考虑使用WorkContinuation.combine()方法,将任务链组合起来使用,如下图所示
比如说上面这幅图,任务的执行顺序为A、B、C、D、E
OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(ConbineWorkerA.class).build();
OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(ConbineWorkerB.class).build();
OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(ConbineWorkerC.class).build();
OneTimeWorkRequest requestD = new OneTimeWorkRequest.Builder(ConbineWorkerD.class).build();
OneTimeWorkRequest requestE = new OneTimeWorkRequest.Builder(ConbineWorkerE.class).build();
//A,B任务链
WorkContinuation continuation1 = WorkManager.getInstance().beginWith(requestA).then(requestB);
//C,D任务链
WorkContinuation continuation2 = WorkManager.getInstance().beginWith(requestC).then(requestD);
//合并上面两个任务链,在接入requestE任务,入队执行
WorkContinuation.combine(continuation1, continuation2).then(requestE).enqueue();
任务链的任务数据流
在任务链中,比如我们有这样的需求,每个任务的数据相互依赖,下一个任务需要依赖上一个任务的输出。其实WorkManager会自动将上一个任务的输出流自动作为下一个任务的输入。
上一个任务调用setOutputData()返回其结果,下一个任务调用getInputData()来获取上一个任务的结果。
A任务
public class A extends Worker {
@NonNull
@Override
public Result doWork() {
//任务执行完成后返回数据
Data outputData = new Data.Builder().putString("aout_put_data","Task1").build();
return Result.success(outputData);
}
}
B任务将获取A任务的数据,拼接一个字符串,并返回给C任务
public class B extends Worker {
@NonNull
@Override
public Result doWork() {
//接收外界传递进行的数据
String inputData = getInputData().getString("aout_put_data");
Data outputData = new Data.Builder().putString("bout_put_data",inputData+"小鑫").build();
return Result.success(outputData);
}
}
C任务
public class B extends Worker {
@NonNull
@Override
public Result doWork() {
//接收外界传递进行的数据
String inputData = getInputData().getString("bout_put_data");
return Result.success();
}
}
执行任务,按照顺序任务,依次执行A、B、C任务即可。
OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(A.class).build();
OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(B.class).build();
OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(C.class).build();
WorkManager.getInstance().beginWith(requestA).then(requestB).then(requestC).enqueue();
组合任务的数据流
为了管理来自多个父级工作请求的输入,WorkManager 使用 InputMerger。
WorkManager 提供两种不同类型的 InputMerger:
- OverwritingInputMerger 会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。
- ArrayCreatingInputMerger 会尝试合并输入,并在必要时创建数组。
OverwritingInputMerger
OverwritingInputMerger 是默认的合并方法。如果合并过程中没有键冲突,键的最新值将覆盖生成的输出数据中的所有先前版本。
例如: 有A、B两个任务合并之后在执行C任务
- A返回的数据为 “akey”:10
- B返回的数据为 “bkey”:20
- 那么C将会收到 “akey”:10 “bkey”:20的数据
ArrayCreatingInputMerger
将每个键与数组配对。如果每个键都是唯一的,您会得到一系列一元数组。
例如: 有A、B两个任务合并之后执行C任务
- A返回的数据为 “akey”:10
- B返回的数据为 “akey”:20
- 那么C将会收一个数据 “akey”:[10,20]
代码如下:
A任务
public class A extends Worker {
@NonNullA
@Override
public Result doWork() {
Data data = new Data.Builder().putInt("akey", 10).build();
return Result.SUCCESS(data);
}
}
B任务
public class A extends Worker {
@NonNullA
@Override
public Result doWork() {
Data data = new Data.Builder().putInt("akey", 20).build();
return Result.SUCCESS(data);
}
}
C任务
public class C extends Worker{
@NonNull
@Override
public Result doWork() {
Data inputData = getInputData();
//因为执行任务 C设置的合并规则为ArrayCreatingInputMerger
//所有C会得到一个数据,有两个值10,20
int[] list = inputData.getIntArray("akey");
return Result.SUCCESS();
}
}
执行任务
OneTimeWorkRequest requestA = new OneTimeWorkRequest.Builder(A.class).build();
OneTimeWorkRequest requestB = new OneTimeWorkRequest.Builder(B.class).build();
// 设置合并规则ArrayCreatingInputMerger
OneTimeWorkRequest requestC = new OneTimeWorkRequest.Builder(C.class)
.setInputMerger(
ArrayCreatingInputMerger.class).build();
//A任务链
WorkContinuation continuationA = WorkManager.getInstance().beginWith(requestA);
//B任务链
WorkContinuation continuationB = WorkManager.getInstance().beginWith(requestB);
//合并上面两个任务链,在接入requestE任务,入队执行
WorkContinuation continuation = WorkContinuation.combine(continuationA, continuationB).then(requestC).
continuation.enqueue();
好了,WorkManager就写到这里了,想更深入了解的,请查看官方文档.
WorkManager官方文档
不足之处,望大家指出来,欢迎留言,谢谢大家。
上一篇: python3 使用OpenCV计算滑块拼图验证码缺口位置(场景示例)
下一篇: linq 行转列