一、引言
WorkManager 是google提供的异步执行任务的管理框架,是 Android Jetpack 的一部分,会根据手机的API版本和应用程序的状态来选择适当的方式执行任务。
在后台执行任务的需求是非常常见的,Android也提供了多种解决方案,如JobScheduler、Loader、Service等,如果这些API没有被恰当使用,则可能会消耗大量电量。Android在解决应用程序耗电问题上做了各种尝试,从Doze到App Standby,通过各种方式限制和管理应用程序,以保证应用程序不会在后台消耗过多的设备电量。WorkManager为应用程序中那些不需要及时完成的任务提供了一个统一的解决方案,以便在设备电量和用户体验之间达到一个比较好的平衡
二、WorkManager特点
- 针对的是不需要及时完成的任务
例如,发送应用程序日志、同步应用程序数据、备份用户数据等,这些任务一般都不需要立即完成,如果我们自己来管理这些任务,逻辑可能会非常复杂,若API使用不恰当,可能会消耗大量电量。 - 保证任务一定会执行
WorkManager能保证任务一定会被执行,即使应用程序当前不在运行中,甚至在设备重启过后任务仍然会在适当的时刻被执行。WorkManager有自己的数据库,关于任务的所有信息和数据都保存在该数据库中。因此只要任务交给了WorkManager,哪怕应用程序彻底退出或者设备被重新启动,WorkManager依然能够保证完成任务。 - 兼容范围广
WorkManager最低能兼容API Level 14,并且不需要设备安装Google Play Services。因此,不用过于担心兼容性问题,因为API Level 14已经能够兼容几乎100%的设备了。
三、WorkManager兼容方案
WorkManager能根据设备的情况,选择不同的执行方案。在API Level 23以上的设备中通过JobScheduler完成任务,在API Level 23以下的设备中,通过AlarmManager和Broadcast Receivers组合来完成任务。但无论采用哪种方案,任务最终都是由Executor来执行的。另外,WorkManager不是一种新的工作线程,它的出现不是为了替代其他类型的工作线程。工作线程通常立即运行,并在任务执行完成后给用户反馈,而WorkManager不是即时的,它不能保证任务能立即得到执行。
四、WorkManager使用方法
1. 使用前的准备
导入依赖
将依赖项添加到应用的 build.gradle 文件中:
dependencies {
def work_version = "2.7.1"
implementation "androidx.work:work-runtime:$work_version"
}
创建Activity
新建一个Activity,在其布局文件中放置一个按钮,作为任务的触发器。
2. 定义任务
新建一个MyWork类继承Worker类,重写dowork()方法,在其中添加希望由WorkManager 运行的工作任务。
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class MyWork extends Worker {
public MyWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
//任务的具体行为
Log.d("tag", "Work doWork");
return Result.success();
}
}
doWork()将返回执行结果Result,会通知WorkManager服务工作是否成功,以及工作失败时是否应重试工作。
- Result.success():工作成功完成。
- Result.failure():工作失败。
- Result.retry():工作失败,应根据其重试策略在其他时间尝试。
3. 配置任务
任务通过WorkRequest在WorkManager中进行定义,为了使用WorkManager调度任务,需要先创建一个WorkRequest对象,将其加入队列。
//为任务创建WorkRequest对象
WorkRequest myWorkRequest = ...
//将任务请求加入队列
WorkManager workManager = WorkManager.getInstance(this);
workManager.enqueue(myWorkRequest);
其中,WorkRequest对象有两种类型:
- OneTimeWorkRequest:只会执行一次的任务请求
- PeriodicWorkRequest:将以周期形式反复执行的任务请求
WorkRequest对象包含WorkManager调度和运行工作所需的所有信息。其中包括运行工作必须满足的约束、调度信息(例如延迟或重复间隔)、重试配置,并且可能包含输入数据(如果工作需要),下面以一次性任务请求为例,介绍各种配置信息的定义方式与实际效果
设置任务触发条件
需要实例化一个Constraints对象指定任务运行的约束(触发条件),常用的约束类型如下:
类型 | 说明 |
---|---|
NetworkType | 约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED) |
BatteryNotLow | 如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行 |
RequiresCharging | 如果设置为 true,那么工作只能在设备充电时运行 |
DeviceIdle | 如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能 |
StorageNotLow | 如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行 |
以NetworkType为例,当将网络的约束设置为无要求时,任务将立刻执行,但当对网络类型有要求时,即使已经处在符合要求的网络环境下,任务也不是立刻执行,而是由系统选择合适的时间再执行。
public void myAddWork(View view) {
//定义触发条件
Constraints constraints = new Constraints.Builder()
//NetworkType.NOT REQUIRED: 对网络没有要求
//NetworkType.CONNECTED: 网络连接的时候执行
//NetworkType.UNMETERED:环计费的网络比如WIFI下执行
//NetworkType.NOT ROAMING:非漫游网络状态执行
//NetworkType.METERED: 计费网络比如3G,4G下执行
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
.build();
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWork.class)
//设置触发条件
.setConstraints(constraints)
.build();
}
执行效果如图:
设置延迟执行任务与任务标签
当队列中的任务满足了全部约束条件后,系统可能会立即运行该任务。可以通过设置延迟时间来使任务经过一段时间后再启动,下面举例说明了如何将任务设置为在加入队列后至少经过5秒后再运行。
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWork.class)
//设置延迟执行
.setInitialDelay(5, TimeUnit.SECONDS)
.build();
运行效果如图:
在WorkRequest中可以为单个任务设置标签,也可以为一组具有逻辑联系的多个任务设置相同的标签,标签可以在任务状态操作中作为任务的标识,可以通过标签来监听任务状态,也可以取消所有具有相同标签的任务。
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWork.class)
//设置tag标签
.addTag("myWorkRequest")
.build();
设置重试和退避策略
当任务的返回值为Result.retry()时,系统需要根据一定的策略来决定每次重试的间隔时间,策略中包含退避延迟时间和退避策略。其中,退避延迟时间指定了首次尝试后重试工作前的最短等待时间,其值不得小于10秒;退避策略定义了退避延迟时间随时间以怎样的方式增长,取值分为两种,线性倍数增长:LINEAR 和指数型增长:EXPONENTIAL。
系统默认的策略是EXPONENTIAL,延迟时间为 10 秒,我们也可以在WorkRequest中自定义策略,下面举例设置了一个线性增长的,等待时间初值为12秒的策略。
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWork.class)
//设置退避策略
.setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofSeconds(12))
.build();
4. 观察任务状态与取消任务
对于只执行一次的任务请求,其生命周期的状态转换如图所示:
对于周期性的任务请求,不存在SUCCESSED和FAILED状态,因为周期性任务永远不会结束,每次运行后,无论结果如何,系统都会重新对其进行调度,周期性任务唯一的终止状态为CANCELLED,其生命周期的状态转换如图所示:
在任务运行的时候,我们可以随时通过任务id或任务标签来查询其状态,利用对应的LiveData方法可以注册监听器来观察WorkInfo的变化。
//观察任务状态
workManager.getWorkInfoByIdLiveData(workRequest.getId()).observe(this, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
Log.d("workInfo", "onChanged: "+workInfo.toString());
}
});
当不再需要运行先前加入队列的任务时,可以根据任务id或任务标签来取消任务,WorkManager 会在后台检查工作的状态。如果工作已经完成,系统不会执行任何操作。否则,工作的状态会更改为CANCELLED,之后就不会运行这个工作。任何依赖于此工作的WorkRequest的状态也将变为CANCELLED。下面举例了在任务运行前将其取消的情况:
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWork.class)
//设置延迟5秒执行
.setInitialDelay(5, TimeUnit.SECONDS)
.build();
//将任务提交给WorkManager
workManager.enqueue(workRequest);
//根据任务id取消任务
new Timer().schedule(new TimerTask() {
@Override
public void run() {
workManager.cancelWorkById(workRequest.getId());
}
}, 2000);
5. 参数传递
WorkManager可以与具体任务之间互相传递参数,在定义WorkRequest对象时可以将参数传入任务,所传参数是一个Data类型的实例化对象。
//定义所要传递的数据
Data input = new Data.Builder()
.putString("inputData", "输入信息")
.build();
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWork.class)
//参数传递
.setInputData(input)
.build();
在任务中可以获得传入的数据并使用,相应的,也可以将数据从任务中传递回WorkManager。
public Result doWork() {
Log.d("tag", "Work doWork");
//获取传递至任务中的数据
String input = getInputData().getString("inputData");
Log.d("input", input);
//定义数据并通过返回值传回WorkManager
Data output = new Data.Builder()
.putString("outputData", "执行成功")
.build();
return Result.success(output);
}
在观察任务状态的监听器中增加以下代码,获取任务传递回来的数据并使用。
workManager.getWorkInfoByIdLiveData(workRequest.getId()).observe(this, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
Log.d("workInfo", workInfo.toString());
//当任务执行状态为SUCCESSED时,获取回传的数据信息
if (workInfo != null && workInfo.getState() == WorkInfo.State.SUCCEEDED){
String output = workInfo.getOutputData().getString("outputData");
Log.d("output", output);
}
}
});
运行结果如图:
6. 配置周期性任务
上述示例均为一次性任务,在实际开发中可能需要定期运行某些任务。例如,定期备份数据、定期下载应用中的新鲜内容或者定期上传日志到服务器,此时则需要用到WorkRequest的另一种类型:PeriodicWorkRequest,创建对象的代码如下:
PeriodicWorkRequest PeriodicRequest =
new PeriodicWorkRequest.Builder(myWorker.class, 2, TimeUnit.HOURS)
// Constraints
.build();
在此示例中,工任务的运行时间间隔定为2小时,需要注意的是,可以定义的间隔时间最短不得少于15分钟。
7. 任务链与任务组合
当需要以特定顺序运行多个任务时,可以创建任务链并将其加入队列,任务链用于指定多个依存任务并定义这些任务的运行顺序。
新建两个类AWorker和BWorker继承Worker类,将其按照任务链形式添加进任务列表
OneTimeWorkRequest ARequest = new OneTimeWorkRequest.Builder(AWorker.class)
.build();
OneTimeWorkRequest BRequest = new OneTimeWorkRequest.Builder(BWorker.class)
.build();
//先运行A,再运行B
WorkManager.getInstance(this)
.beginWith(ARequest)
.then(BRequest)
.enqueue();
运行结果如图:
有时一些任务需要在若干个任务执行完毕后方可执行,此时就需要用到任务组合。定义五个任务来模拟图中的顺序执行情况
//创建五个任务请求
OneTimeWorkRequest ARequest = new OneTimeWorkRequest.Builder(AWorker.class)
.build();
OneTimeWorkRequest BRequest = new OneTimeWorkRequest.Builder(BWorker.class)
.build();
OneTimeWorkRequest CRequest = new OneTimeWorkRequest.Builder(CWorker.class)
.build();
OneTimeWorkRequest DRequest = new OneTimeWorkRequest.Builder(DWorker.class)
.build();
OneTimeWorkRequest ERequest = new OneTimeWorkRequest.Builder(EWorker.class)
.build();
//将同一条任务链上的任务执行顺序定义成一个任务组合
WorkContinuation continuation1 = WorkManager.getInstance(this)
.beginWith(ARequest)
.then(BRequest);
WorkContinuation continuation2 = WorkManager.getInstance(this)
.beginWith(CRequest)
.then(DRequest);
//把两个任务组合放入一个集合
List<WorkContinuation> taskList = new ArrayList<>();
taskList.add(continuation1);
taskList.add(continuation2);
//设置当集合中的所有任务组合都运行完毕后,才运行任务E
WorkContinuation.combine(taskList)
.then(ERequest)
.enqueue();
运行结果如图:
作者:凌语桐
原文链接:https://blog.csdn.net/pandcu/article/details/128167687?spm=1001.2014.3001.5502