一、WorkManager是什么?
WorkManager 是一个 API,使您可以轻松调度那些即使在退出应用或重启设备时仍应运行的可延期异步任务。WorkManager API 是一个针对先前的 Android 后台调度 API(包括 FirebaseJobDispatcher、GcmNetworkManager 和 JobScheduler)的合适的建议替换组件。WorkManager 在新版一致性 API 中整合了其前身的功能,该 API 支持 API 级别 14,同时可保证电池续航时间。.
在后台,WorkManager 根据以下条件使用底层作业调度服务:
.
.
二、作用
除了具备更为简单且一致的 API 之外,WorkManager 还具备许多其他关键优势,其中包括:
1. 工作约束
使用工作约束明确定义工作运行的最佳条件。(例如,仅在设备采用 Wi-Fi 网络连接时、当设备处于空闲状态或者有足够的存储空间时运行。)
2. 强大的调度
WorkManager允许您使用灵活的调度窗口调度工作,以运行一次性或重复工作。还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。已调度的工作存储在内部托管的 SQLite 数据库中,由 WorkManager 负责确保该工作持续进行,并在设备重新启动后重新调度。此外,WorkManager 遵循低电耗模式等省电功能和最佳做法,因此您在这方面无需担心。
3. 灵活的重试政策
有时工作会失败。WorkManager 提供了灵活的重试政策,包括可配置的指数退避政策。
4. 工作链接
对于复杂的相关工作,请使用流畅自然的界面将各个工作任务链接在一起,这样您便可以控制哪些部分依序运行,哪些部分并行运行。对于每项工作任务,您可以定义工作的输入和输出数据。将工作链接在一起时,WorkManager 会自动将输出数据从一个工作任务传递给下一个工作任务。
优点
-
WorkManager对比JobScheduler, AlarmManger的优势:我们要知道虽然AlarmManager是一直存在但是JobScheduler是Android 5.x之后才有的。WorkManager的底层实现,会根据你的设备API的情况,自动选用JobScheduler, 或是AlarmManager来实现后台任务。
-
WorkManager对比AsyncTask, ThreadPool的优势:WorkManager里面的任务在应用退出之后还可以继续执行。AsyncTask, ThreadPool里面的任务在应用退出之后不会执行。
WorkManager适用于那些在应用退出之后任务还需要继续执行的需求(比如应用数据上报服务器的情况),对应那些在应用退出的之后任务也需要终止的情况就需要选择ThreadPool、AsyncTask来实现。
.
.
三、使用步骤
1.引入库
将以下依赖项添加到应用的 build.gradle 文件中:
dependencies {
def work_version = "2.4.0"
implementation "androidx.work:work-runtime:$work_version"
androidTestImplementation "androidx.work:work-testing:$work_version"
}
注意:
您始终可以在 WorkManager 版本页面上找到最新版本的 WorkManager,包括 Beta 版、Alpha 版和候选版本。
2.定义Worker 类
工作使用 Worker 类定义。doWork() 方法在 WorkManager 提供的后台线程上同步运行
代码如下(示例):
public class MyWorker extends Worker {
public MyWorker (@NonNull Context context,@NonNull WorkerParameters params) {
super(context, params);
}
@Override
/**
* 任务逻辑
* @return 任务的执行情况,成功,失败,还是需要重新执行
*/
public Result doWork() {
//需要后台执行的任务的逻辑写在下面
...
// 返回任务执行的结果
return Result.success();
}
}
从 doWork() 返回的 Result 会通知 WorkManager 服务工作是否成功,以及工作失败时是否应重试工作。
- Result.success():工作成功完成。
- Result.failure():工作失败。
- Result.retry():工作失败,应根据其重试政策在其他时间尝试。
2.创建 WorkRequest
定义工作后,必须使用 WorkManager 服务进行调度该工作才能运行。对于如何调度工作,WorkManager 提供了很大的灵活性。您可以将其安排为在某段时间内定期运行,也可以将其安排为仅运行一次。
WorkRequest是一个抽象类,组件里面也给两个相应的子类:OneTimeWorkRequest(任务只执行一遍)、PeriodicWorkRequest(任务周期性的执行)
不论您选择以何种方式调度工作,请始终使用 WorkRequest。Worker 定义工作单元,WorkRequest(及其子类)则定义工作运行方式和时间。在最简单的情况下,您可以使用 OneTimeWorkRequest
代码如下(示例):
WorkRequest MyWorkRequest =
new OneTimeWorkRequest.Builder(MyWorker.class)
.build();
3.将 WorkRequest 提交给系统
最后,您需要使用 enqueue() 方法将 WorkRequest 提交到 WorkManager
WorkManager
.getInstance(myContext)//传递参数给Worker 类
.enqueue(MyWorkRequest);//提交给系统
执行工作器的确切时间取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供最佳行为。
总结
到这里WorkManager的简单使用就介绍完了,下面再介绍WorkManager相关类的作用和注意点。
.
.
四、 WorkManager相关类介绍
1. Worker类
Worker类用于指定需要执行的具体任务。任务的具体逻辑在Worker里面写,Worker是个抽象类。所以我们需要继承并实现这个类在定义自己的任务。
Worker类里面几个比较关键的函数:任务逻辑实现函数,任务输入数据的获取函数,任务输出数据的设置函数。
Worker类的源码:
/**
* 任务逻辑
* @return 任务的执行情况,成功,失败,还是需要重新执行
*/
@WorkerThread
public abstract @NonNull Worker.Result doWork();
/**
* 任务的输入数据,有的时候可能需要我们传递参数进去,比如下载文件我们需要传递文件路基进去,
* 在doWork()函数中通过getInputData()获取到我们传递进来的参数
* @return Data参数
*/
public final @NonNull Data getInputData() {
return mExtras.getInputData();
}
/**
* 设置我们任务输出结果
* @param outputData 结果
*/
public final void setOutputData(@NonNull Data outputData) {
mOutputData = outputData;
}
其中doWork()函数的返回值:
- Worker.Result.SUCCESS:任务执行成功。
- Worker.Result.FAILURE:任务执行失败。
- Worker.Result.RETRY:任务需要重新执行,如果出现这个返回结果,就需要与WorkRequest.Builder中的setBackoffCriteria()函数一起使用。
定义 Worker类
前台和后台服务,有时候需要传入数据,在Activity定义Data,将需要传入的数据包装一下,然后通过WorkRequest的setInputData()传入
Data data = new Data.Builder().putString("params", "hello").build();
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(MyWorker.class, 15, TimeUnit.SECONDS)
.setInputData(data)
.build();
这个传入的值,可以在Worker中获取
public class MyWorker extends Worker {
public MyWorker (@NonNull Context context,@NonNull WorkerParameters params) {
super(context, params);
}
@Override
//任务逻辑
public Result doWork() {
//获取WorkRequest传递过来的参数
String params = getInputData().getString("params");
//需要后台执行的任务的逻辑写在下面
...
// 返回任务执行的结果
return Result.success();
}
}
2. WorkRequest对象
WorkRequest代表一个单独的任务,是对Worker任务的包装,一个WorkRequest对应一个Worker类。我们可以通过WorkRequest来给Worker类添加约束细节,比如指定任务应该运行的环境,任务的输入参数,任务只有在有网的情况下执行等等。WorkRequest是一个抽象类,组件里面也给两个相应的子类:OneTimeWorkRequest(任务只执行一遍)、PeriodicWorkRequest(任务周期性的执行)。
- WorkRequest.Builder: 创建WorkRequest对象的帮助类。
- Constraints:指定任务运行的限制条件(例如,”仅当连接到网络时”)。使用Constraint.Builder来创建Constraints,并在创建WorkRequest之前把Constraints传给WorkRequest.Builder的setConstraints()函数。
WorkRequest 对象可以处理常见用例,例如:
- 调度一次性工作和重复性工作
- 设置工作约束条件,例如要求连接到 Wi-Fi 网络或正在充电
- 确保至少延迟一定时间再执行工作
- 设置重试和退避策略
- 将输入数据传递给工作
- 使用标记将相关工作分组在一起
WorkRequest里面常用函数介绍:
// 获取 WorkRequest对应的UUID
public @NonNull UUID getId();
//获取 WorkRequest对应的UUID string
public @NonNull String getStringId();
//获取WorkRequest对应的WorkSpec(包含了任务的一些详细信息)
public @NonNull WorkSpec getWorkSpec();
//获取 WorkRequest对应的tag
public @NonNull Set<String> getTags();
//设置任务的退避/重试策略
public @NonNull B setBackoffCriteria(@NonNull BackoffPolicy backoffPolicy, long backoffDelay,@NonNull TimeUnit timeUnit);
//设置任务的运行的限制条件,比如有网的时候执行任务,不是低电量的时候执行任务
public @NonNull B setConstraints(@NonNull Constraints constraints);
//设置任务的输入参数
public @NonNull B setInputData(@NonNull Data inputData);
//设置任务的tag
public @NonNull B addTag(@NonNull String tag);
//设置任务结果保存时间
public @NonNull B keepResultsForAtLeast(long duration, @NonNull TimeUnit timeUnit);
@RequiresApi(26)
public @NonNull B keepResultsForAtLeast(@NonNull Duration duration);
}
WorkRequest 对象的具体的功能的使用:
- 调度一次性工作
对于无需额外配置的简单工作,请使用静态方法 from:
WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);
对于更复杂的工作,可以使用构建器
WorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
//额外的配置的功能
...
.build();
- 调度定期工作
您的应用有时可能需要定期运行某些工作。例如,您可能要定期备份数据、定期下载应用中的更新内容或者定期上传日志到服务器
使用 PeriodicWorkRequest 创建定期执行的 WorkRequest 对象的方法如下:
PeriodicWorkRequest saveRequest =new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS).build();
注意:
可以定义的最短重复间隔是 15 分钟(与 JobScheduler API 相同)
- 灵活的运行间隔
如果您的工作的性质致使其对运行时间敏感,您可以将 PeriodicWorkRequest 配置为在每个时间间隔的灵活时间段内运行,如图 1 所示
图 1. 此图显示了可在灵活时间段内运行工作的的重复间隔。
如需定义具有灵活时间段的定期工作,请在创建 PeriodicWorkRequest 时传递 flexInterval 以及 repeatInterval。灵活时间段从 repeatInterval - flexInterval 开始,一直到间隔结束。
以下是可在每小时的最后 15 分钟内运行的定期工作的示例:
WorkRequest saveRequest =
new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class,
1, TimeUnit.HOURS,
15, TimeUnit.MINUTES)
.build();
注意:
重复间隔必须大于或等于 PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,而灵活间隔必须大于或等于 PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS。
- 约束对定期工作的影响
您可以对定期工作设置约束。例如,您可以为工作请求添加约束,以便工作仅在用户设备充电时运行
常见工作约束
- NetworkType: 约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)
- BatteryNotLow: 如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行
- RequiresCharging: 如果设置为 true,那么工作只能在设备充电时运行
- DeviceIdle: 如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。如果您要运行批量操作,否则可能会降低用户设备上正在积极运行的其他应用的性能,建议您使用此约束
- StorageNotLow: 如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行
如需创建一组约束并将其与某项工作相关联,请使用一个 Contraints.Builder() 创建 Constraints 实例,并将该实例分配给 WorkRequest.Builder()。
例如,以下代码会构建了一个工作请求,该工作请求仅在用户设备正在充电且连接到 Wi-Fi 网络时才会运行:
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build();
WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
.setConstraints(constraints)
.build();
- 延迟工作
如果工作没有约束,或者当工作加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动
下面举例说明了如何将工作设置为在加入队列后至少经过 10 分钟后再运行
WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
.setInitialDelay(10, TimeUnit.MINUTES)
.build();
注意:
执行工作器的确切时间还取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供可能的最佳行为
- 重试和退避政策
如果您需要让 WorkManager 重试工作,可以从工作器返回
Result.retry()。然后,系统将根据退避延迟时间和退避政策重新调度工作。
- 退避延迟时间指定了首次尝试后重试工作前的最短等待时间。此值不能超过 10 秒(或 MIN_BACKOFF_MILLIS)。
- 退避政策定义了在后续重试过程中,退避延迟时间随时间以怎样的方式增长。WorkManager 支持 2 个退避政策,即 LINEAR 和
EXPONENTIAL。每个工作请求都有退避政策和退避延迟时间。默认政策是 EXPONENTIAL,延迟时间为 10 秒,但您可以在工作请求配置中替换此设置。
WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build();
注意:
退避延迟时间不精确,在两次重试之间可能会有几秒钟的差异,但绝不会低于配置中指定的初始退避延迟时间
- 标记工作
每个工作请求都有一个唯一标识符,该标识符可用于在以后标识该工作,以便取消工作或观察其进度
以下代码表示向工作添加标记:
WorkRequest myWorkRequest =
new OneTimeWorkRequest.Builder(MyWork.class)
.addTag("myTag")
.build();
- 传递数据给Worker类
您的工作可能需要输入数据才能正常运行。例如,处理图片上传的工作可能需要使用待上传图片的 URI 作为输入数据。
输入值以键值对的形式存储在 Data 对象中,并且可以在工作请求中设置。WorkManager 会在执行工作时将输入 Data 传递给工作。Worker 类可通过调用 Worker.getInputData() 访问输入参数。以下代码展示了如何创建需要输入数据的 Worker 实例,以及如何在工作请求中发送该实例。
public class MyWork extends Worker {
public MyWork(Context appContext, WorkerParameters workerParams) {
super(appContext, workerParams);
}
@NonNull
@Override
public Result doWork() {
String imageUriInput = getInputData().getString("IMAGE_URI");
if(imageUriInput == null) {
return Result.failure();
}
uploadFile(imageUriInput);
//把数据返回,给Activity
Data output = new Data.Builder()
.putInt("key", Value)
.build();
return Result.success(output);
}
...
}
//在Activity中传递数据给MyWork类
Data data = new Data.Builder().putString("萝莉卡哇伊").build()
WorkRequest myUploadWork =
new OneTimeWorkRequest.Builder(MyWork.class)
.setInputData(
new Data.Builder()
.putString(data)
.build()
)
.build();
//接收任务结束后返回的数据
WorkManager.getInstance(myContext).getWorkInfoByIdLiveData(mathWork.getId())
.observe(lifecycleOwner, info -> {
if (info != null && info.getState().isFinished()) {
//接收返回的数据
String myResult = info.getOutputData().getInt("key", Value));
...
}
});
3.WorkManager对象
管理任务请求和任务队列,我们需要把WorkRequest对象传给WorkManager以便将任务编入队列。通过WorkManager来调度任务,以分散系统资源的负载
WorkManager常用函数介绍
//任务入队
public final void enqueue(@NonNull WorkRequest... workRequests);
public abstract void enqueue(@NonNull List<? extends WorkRequest> workRequests);
/**
* 链式结构的时候使用,从哪些任务开始。
* 比如我们有A,B,C三个任务,我们需要顺序执行。那我们就可以WorkManager.getInstance().beginWith(A).then(B).then(C).enqueue();
*/
public final @NonNull WorkContinuation beginWith(@NonNull OneTimeWorkRequest...work);
public abstract @NonNull WorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work);
//创建一个唯一的工作队列,唯一工作队列里面的任务不能重复添加
public final @NonNull WorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull OneTimeWorkRequest... work);
public abstract @NonNull WorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> work);
public abstract void enqueueUniquePeriodicWork(
@NonNull String uniqueWorkName,
@NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
@NonNull PeriodicWorkRequest periodicWork);
//通过UUID取消任务
public abstract void cancelWorkById(@NonNull UUID id);
//通过tag取消任务
public abstract void cancelAllWorkByTag(@NonNull String tag);
// 取消唯一队列里面所有的任务(beginUniqueWork)
public abstract void cancelUniqueWork(@NonNull String uniqueWorkName);
//取消所有的任务
public abstract void cancelAllWork();
//获取任务的WorkStatus。一般会通过WorkStatus来获取返回值,LiveData是可以感知WorkStatus数据变化的
public abstract @NonNull LiveData<WorkStatus> getStatusById(@NonNull UUID id);
public abstract @NonNull LiveData<List<WorkStatus>> getStatusesByTag(@NonNull String tag);
//获取唯一队列里面所有的任务(beginUniqueWork)的WorkStatus
public abstract @NonNull LiveData<List<WorkStatus>> getStatusesForUniqueWork(@NonNull String uniqueWorkName);
WorkManager对象里的方法的简单介绍
- 唯一工作
唯一工作既可用于一次性工作,也可用于定期工作。您可以通过调用以下方法之一创建唯一工作序列,具体取决于您是调度重复工作还是一次性工作
- WorkManager.enqueueUniqueWork()(用于一次性工作)
- WorkManager.enqueueUniquePeriodicWork()(用于定期工作)
这两种方法都接受 3 个参数:
- uniqueWorkName - 用于唯一标识工作请求的 String。
- existingWorkPolicy - 此 enum 可告知 WorkManager 如果已有使用该名称且尚未完 成的唯一工作链,应执行什么操作。如需了解详情,请参阅冲突解决政策。
- work - 要调度的 WorkRequest
PeriodicWorkRequest myWorkRequest = new
PeriodicWorkRequest.Builder(SendLogsWorker.class, 24, TimeUnit.HOURS)
.setConstraints(new Constraints.Builder()
.setRequiresCharging(true)
.build()
)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"sendLogs",//uniqueWorkName
ExistingPeriodicWorkPolicy.KEEP,//existingWorkPolicy
myWorkRequest //work
);
现在,如果上述代码在 sendLogs 作业已处于队列中的情况下运行,系统会保留现有的作业,并且不会添加新的作业
冲突解决政策
调度唯一工作时,您必须告知 WorkManager 在发生冲突时要执行的操作。您可以通过在将工作加入队列时传递一个枚举来实现此目的
对于一次性工作,您需要提供一个 ExistingWorkPolicy,它支持用于处理冲突的 4 个选项
- REPLACE:用新工作替换现有工作。此选项将取消现有工作。
- KEEP: 保留现有工作,并忽略新工作。
- APPEND: 将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。
对于定期工作,您需要提供一个 ExistingPeriodicWorkPolicy,它支持 REPLACE 和 KEEP 这两个选项。这些选项的功能与其对应的 ExistingWorkPolicy 功能相同
- 观察您的任务的工作情况
在将工作加入队列后,您可以随时按其 name、id 或与其关联的 tag 在 WorkManager 中进行查询,以检查其状态
//获取工作的ID
workManager.getWorkInfoById(myWorker.id);
// 获取工作的名字
workManager.getWorkInfosForUniqueWork("myWorker");
// 获取工作的的标记
workManager.getWorkInfosByTag("myWorkerTag");
该查询会返回 WorkInfo 对象的 ListenableFuture,该值包含工作的 id、其标记、其当前的 State 以及通过 Result.success(outputData) 设置的任何输出数据。
或者
您可以通过注册监听器来观察 WorkInfo 的变化。例如,如果您想要在某项工作成功完成后向用户显示消息,您可以进行如下设置:
workManager.getWorkInfoByIdLiveData(myWorker.id)
.observe(getViewLifecycleOwner(), workInfo -> {
if (workInfo.getState() != null &&
workInfo.getState() == WorkInfo.State.SUCCEEDED) {
Snackbar.make(requireView(),
R.string.work_completed, Snackbar.LENGTH_SHORT)
.show();
}
});
- 取消和停止工作
如果您不再需要运行先前加入队列的工作,则可以要求将其取消。您可以按工作的 name、id 或与其关联的 tag 取消工作
// 通过ID来取消工作
workManager.cancelWorkById(myWorker.id);
// 通过名字来取消工作
workManager.cancelUniqueWork("myWorker");
// 通过标记来取消工作
workManager.cancelAllWorkByTag("myWorkerTag");
- 停止正在运行的工作器
正在运行的 Worker 可能会由于以下几种原因而停止运行:
- 您明确要求取消它(例如,通过调用 WorkManager.cancelWorkById(UUID) 取消)。
- 如果是唯一工作,您明确地将 ExistingWorkPolicy 为 REPLACE 的新 WorkRequest 加入到了队列中。旧的 WorkRequest 会立即被视为已取消。
- 您的工作约束条件已不再满足。
- 系统出于某种原因指示您的应用停止工作。如果超过 10 分钟的执行期限,可能会发生这种情况。该工作会调度为在稍后重试。
在这些情况下,您的工作器会停止
4.WorkStatus
包含任务的信息。WorkManager为每个WorkRequest对象提供一个LiveData(WorkManager通过getStatusById、getStatusesByTag、getStatusesForUniqueWork函数来获取)。LiveData持有一个WorkStatus对象。LiveData是可以感知数据变化的。通过观察这个LiveData,我们可以确定任务的当前状态,并在任务完成后获得返回值。WorkStatus里面就包含的东西不多就任务的id、tag、状态、返回值
通过如下方式来监听任务的状态
// 获取到LiveData然后监听数据变化
WorkManager.getInstance().getStatusById(request.getId()).observe(this, new Observer<WorkStatus>() {
@Override
public void onChanged(@Nullable WorkStatus workStatus) {
if (workStatus == null) {
return;
}
if (workStatus.getState() == State.ENQUEUED) {
Log.i("workStatus","任务入队");
}
if (workStatus.getState() == State.RUNNING) {
Log.i("workStatus","任务正在执行");
}
if (workStatus.getState().isFinished()) {
Data data = workStatus.getOutputData();
Log.i("workStatus","任务完成" + "-结果:" + data.getString("key_name", "null"));
}
}
});
5.Data类
Data是用于来给Worker设置输入参数和输出参数的。举个例子,比如我们需要去网络上下载图,那么需要给Worker传入下载地址(输入参数),在Worker执行成功之后我们又需要获取到图片在本地的保持路径(输出参数)。这这个传入传出都是通过Data来实现的。
注意:
Data是一个轻量级的容器(不能超过10KB),Data通过key-value的形式来保存信息
总结
到这里WorkManager相关类的作用和注意点基本都介绍完毕了,具体的细节请阅读WorkManager使用指南。