管理工作
在上一章的最后,我们提到了一句enqueueUniqueWork(),那么enqueueUniqueWork()是怎么用的呢?
唯一Worker
当把工作加入队列的最简单的方法就是调用enqueue()方法,但是这种情况下,有可能将同一个作业多次加入到队列中,所以你可以通过创建一个唯一工作序列,确保同一时刻只有一个具有特定名称的工作实例。
与ID不同,唯一名称是开发者指定的,而不是由WorkManager自动生成的;与Tag也不同,唯一名称仅与一个工作实例相关联。
创建唯一工作的方法:
•WorkManager.enqueueUniqueWork(用于一次性工作)
•WorkManager.enqueueUniquePeriodicWork(用于定期工作)
这两种方法都有3个参数:
•uniqueWorkName - 用于标识唯一工作请求的String。
•existingWorkPolicy - 用于如果已有使用该名称且尚未完成的唯一工作(链),应该执行什么操作。参照冲突解决政策。
•work - 需要调度的WorkRequest。
借助唯一工作,我们可以解决前面提到的重复调度问题。
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"sendLogs",
ExistingPeriodicWorkPolicy.KEEP,
sendLogsWorkRequest);
如上面的代码,现在如果sendLogs作业已经处于队列中,系统就会保留现有的作业,而不会添加新的作业。
冲突解决政策
调度唯一工作时,您必须告知 WorkManager 在发生冲突时要执行的操作。您可以通过在将工作加入队列时传递一个枚举来实现此目的。
对于一次性工作,您需要提供一个ExistingWorkPolicy,它有4个选项。
•REPLACE: 用新工作替换现有工作。此选项将取消现有工作。
•KEEP: 保留现有工作,并忽略新工作。
•APPEND: 将新工作附加到现有工作的末尾。此选项会将新工作链接到现有工作,并在现有工作完成后运行。并且现有工作将成为新工作的先决条件。如果现有工作为CANCELLED或者FAILED状态,新工作也会变为CANCELLED或者FAILED状态。
•APPEND_OR_REPLACE: 此选项类似于APPEND,不过它不依赖于现有工作的状态。即使现有工作为CANCELLED或者FAILED状态,新工作仍旧会运行。
REPLACE
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.KEEP, (OneTimeWorkRequest) uploadWorkRequest3);
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.REPLACE, (OneTimeWorkRequest) uploadWorkRequest4);
workManager.getWorkInfosForUniqueWorkLiveData("work&work").observe(this, info -> {
if (info != null) {
for (int i = 0; i < info.size(); i++) {
if (state == WorkInfo.State.SUCCEEDED) {
Log.d(TAG, "WorkRequest " + info.get(i).getOutputData().getString("out_key") + " is succeeded");
}
}
}
});
如上面的代码,我们定义两个WorkRequest,并且第一个WorkRequest定义为KEEP,第二个定义为REPLACE,我们发现打印出来的Log:
2021-01-11 02:55:16.497 14992-14992/com.example.myapplication D/MainActivity: WorkRequest 4_out is succeeded
WorkRequest3的确被替换了,毕竟只有4被打印出来。
KEEP
现在我们修正上面的代码,WorkRequest都修正为KEEP。
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.KEEP, (OneTimeWorkRequest) uploadWorkRequest3);
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.KEEP, (OneTimeWorkRequest) uploadWorkRequest4);
打印出来的Log:
2021-01-11 03:03:28.143 15550-15550/com.example.myapplication D/MainActivity: WorkRequest 3_out is succeeded
APPEND
同上,修正第二个WorkRequest为APPEND。
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.KEEP, (OneTimeWorkRequest) uploadWorkRequest3);
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.APPEND, (OneTimeWorkRequest) uploadWorkRequest4);
打印出来的Log:
2021-01-11 03:05:56.748 15903-15903/com.example.myapplication D/MainActivity: WorkRequest 3_out is succeeded
2021-01-11 03:06:06.833 15903-15903/com.example.myapplication D/MainActivity: WorkRequest 4_out is succeeded
发现WorkRequest4的确接在3后面执行了。下面我们验证下如果现有工作为CANCELLED或者FAILED状态,新工作也会变为CANCELLED或者FAILED状态
这句话。
修改代码如下:
WorkRequest uploadWorkRequest3 =
new OneTimeWorkRequest.Builder(UploadWorker.class)
.setInitialDelay(10, TimeUnit.SECONDS)
.setInputData(data3)
.addTag("work_work")
// Additional configuration
.build();
Log.d(TAG, "WorkRequest 3 id is " + uploadWorkRequest3.getId())
WorkRequest uploadWorkRequest4 = ......
Log.d(TAG, "WorkRequest 4 id is " + uploadWorkRequest4.getId());
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.KEEP, (OneTimeWorkRequest) uploadWorkRequest3);
workManager.getWorkInfosForUniqueWorkLiveData("work&work").observe(this, info -> {
if (info != null) {
for (int i = 0; i < info.size(); i++) {
WorkInfo.State state = info.get(i).getState();
Log.d(TAG, "WorkInfo.State is " + state);
if (state == WorkInfo.State.CANCELLED) {
Log.d(TAG, "WorkRequest " + info.get(i).getId() + " is cancelled");
}
}
}
});
handler.postDelayed(new Runnable() {
@Override
public void run() {
workManager.cancelUniqueWork("work&work");
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.APPEND, (OneTimeWorkRequest) uploadWorkRequest4);
}
}, 5000);
上面的代码中,我们通过Id来区分下两个WorkRequest,看看是不是两个WorkRequest都被cancel掉了。Log:
2021-01-11 04:02:57.048 18699-18699/com.example.myapplication D/MainActivity: WorkRequest 3 id is ea2929a7-433e-4a74-a5e4-3e822dd9aa99
2021-01-11 04:02:57.048 18699-18699/com.example.myapplication D/MainActivity: WorkRequest 4 id is f00b197f-6bd3-448d-b438-6f2ea6585892
2021-01-11 04:03:02.135 18699-18699/com.example.myapplication D/MainActivity: WorkInfo.State is CANCELLED
2021-01-11 04:03:02.135 18699-18699/com.example.myapplication D/MainActivity: WorkRequest ea2929a7-433e-4a74-a5e4-3e822dd9aa99 is cancelled
2021-01-11 04:03:02.135 18699-18699/com.example.myapplication D/MainActivity: WorkInfo.State is CANCELLED
2021-01-11 04:03:02.136 18699-18699/com.example.myapplication D/MainActivity: WorkRequest f00b197f-6bd3-448d-b438-6f2ea6585892 is cancelled
根据Log,两个WorkRequest的确都被Cancel了。
APPEND_OR_REPLACE
这个选项就不验证Success的状态了,验证下即使现有工作为CANCELLED或者FAILED状态,新工作仍旧会运行
这句话。
修改代码,把APPEND换成APPEND_OR_REPLACE。
workManager.cancelUniqueWork("work&work");
workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.APPEND_OR_REPLACE, (OneTimeWorkRequest) uploadWorkRequest4);
Log:
2021-01-11 04:13:09.812 19563-19563/com.example.myapplication D/MainActivity: WorkRequest 3 id is 313fb355-7f17-4b56-aa04-d87ce061fa6c
2021-01-11 04:13:09.812 19563-19563/com.example.myapplication D/MainActivity: WorkRequest 4 id is b590301b-2df6-4198-b784-372280cf8cbe
2021-01-11 04:13:24.918 19563-19563/com.example.myapplication D/MainActivity: WorkInfo.State is SUCCEEDED
2021-01-11 04:13:24.918 19563-19563/com.example.myapplication D/MainActivity: WorkRequest b590301b-2df6-4198-b784-372280cf8cbe is succeeded
通过Log,WorkRequest4的确没有受到WorkRequest3的Cancel的影响,WorkRequest4成功的返回Success。
对于定期工作,需要提供ExistingPeriodicWorkpolicy,它支持REPLACE和KEEP两个选项。这些选项的功能和对应的ExistingWorkPolicy功能相同。
监视工作
在上面的代码中,我们已经使用到观察工作的方法,这边进行一个整理。
在将工作加入队列后,你可以额随时按其name、id或者与其关联的tag在WorkManager中进行查询,和检查其状态。
// by id
workManager.getWorkInfoById(syncWorker.id); // ListenableFuture<WorkInfo>
// by name
workManager.getWorkInfosForUniqueWork("sync"); // ListenableFuture<List<WorkInfo>>
// by tag
workManager.getWorkInfosByTag("syncTag"); // ListenableFuture<List<WorkInfo>>
该查询会返回WorkInfo对象的ListenableFuture,该值包含工作的id、标记、当前的state和通过Result.success(outputData)设置的任何输出数据。
复杂的工作查询条件
除了上面一开始讲的通过id、name或者tag进行查询之外。在WorkManager2.4.0和更高版本中支持使用WorkQuery对象对已经加入队列的作业进行复查查询。WorkQuery支持按照工作的标记、状态和唯一工作名称的组合进行查询。
以下示例说明了如何查找带有“syncTag”标记、处于 FAILED
或 CANCELLED
状态,且唯一工作名称为“preProcess”或“sync”的所有工作。
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);
WorkQuery中的每个组件与其他组件都是AND逻辑关系。组件中的每个值都是OR的逻辑关系。例: (name1 OR name2 OR ...) AND (tag1 OR tag2 OR ...) AND (state1 OR state2 OR ...)
观察工作的中间进度
WorkManager2.3.0-alpha01为设置和观察Worker的中间进度添加了支持。如果应用在前台运行时,工作器保持运行状态,那么也可以使用返回 WorkInfo 的 LiveData的 API 向用户显示此信息。
Worker 现在支持 setProgressAsync() API,此类 API 允许保留中间进度。借助这些 API,开发者能够设置可通过界面观察到的中间进度。进度由 Data类型表示,这是一个可序列化的属性容器(类似于 input和 output,并且受到相同的限制)。
只有在 Worker 运行时才能观察到和更新进度信息。如果尝试在 Worker 完成执行后在其中设置进度,则将会被忽略。您还可以使用 getWorkInfoBy…()或 getWorkInfoBy…LiveData() 方法来观察进度信息。这两个方法会返回 WorkInfo 的实例,后者有一个返回 Data
的新 getProgress()) 方法。
更新进度
对于使用 ListenableWorker或 Worker的 Java 开发者,setProgressAsync()API 会返回 ListenableFuture<Void>
;更新进度是异步过程,因为更新过程涉及将进度信息存储在数据库中。在 Kotlin 中,您可以使用 CoroutineWorker对象的 setProgress()扩展函数来更新进度信息。
此示例展示了一个简单的 ProgressWorker
。Worker
在启动时将进度设置为 0,在完成后将进度值更新为 100。
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class ProgressWorker extends Worker {
private static final String PROGRESS = "PROGRESS";
private static final long DELAY = 1000L;
public ProgressWorker(
@NonNull Context context,
@NonNull WorkerParameters parameters) {
super(context, parameters);
// Set initial progress to 0
setProgressAsync(new Data.Builder().putInt(PROGRESS, 0).build());
}
@NonNull
@Override
public Result doWork() {
try {
// Doing work.
Thread.sleep(DELAY);
} catch (InterruptedException exception) {
// ... handle exception
}
// Set progress to 100 after you are done doing your work.
setProgressAsync(new Data.Builder().putInt(PROGRESS, 100).build());
return Result.success();
}
}
观察进度
观察进度信息也很简单。您可以使用 getWorkInfoBy…()或 getWorkInfoBy…LiveData() 方法,并引用 WorkInfo。
以下是使用 getWorkInfoByIdLiveData
API 的示例。
WorkManager.getInstance(getApplicationContext())
// requestId is the WorkRequest id
.getWorkInfoByIdLiveData(requestId)
.observe(lifecycleOwner, new Observer<WorkInfo>() {
@Override
public void onChanged(@Nullable WorkInfo workInfo) {
if (workInfo != null) {
Data progress = workInfo.getProgress();
int value = progress.getInt(PROGRESS, 0)
// Do something with progress
Log.d(TAG, "value is " + value);
}
}
});
**※※※**上面的代码是官方文档中给的示例代码,我把代码放到AndroidStudio中运行,发现100一直都打印不出来。
我就修正ProgressWorker的doWork()方法,修正代码如下:
@NonNull
@Override
public Result doWork() {
try {
// Doing work.
setProgressAsync(new Data.Builder().putInt(PROGRESS, 50).build());
Thread.sleep(DELAY);
} catch (InterruptedException exception) {
// ... handle exception
}
// Set progress to 100 after you are done doing your work.
setProgressAsync(new Data.Builder().putInt(PROGRESS, 70).build());
try {
Thread.sleep(DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
setProgressAsync(new Data.Builder().putInt(PROGRESS, 100).build());
try {
Thread.sleep(DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Result.success();
}
同时在MainActivity中打印value的上一行打印worker的state。
这种情况下,100能够被输出。Log:
2021-01-11 06:48:52.798 27355-27355/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is RUNNING
2021-01-11 06:48:52.798 27355-27355/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 0
2021-01-11 06:48:52.803 27355-27355/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is RUNNING
2021-01-11 06:48:52.803 27355-27355/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 50
2021-01-11 06:48:54.299 27355-27355/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is RUNNING
2021-01-11 06:48:54.299 27355-27355/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 70
2021-01-11 06:48:55.812 27355-27355/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is RUNNING
2021-01-11 06:48:55.813 27355-27355/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 100
2021-01-11 06:48:57.356 27355-27355/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is SUCCEEDED
2021-01-11 06:48:57.356 27355-27355/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 0
而如果把最后一个sleep给注释掉,100又不能被输出了。Log:
2021-01-11 06:49:42.264 27443-27443/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is RUNNING
2021-01-11 06:49:42.265 27443-27443/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 0
2021-01-11 06:49:42.272 27443-27443/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is RUNNING
2021-01-11 06:49:42.272 27443-27443/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 50
2021-01-11 06:49:43.773 27443-27443/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is RUNNING
2021-01-11 06:49:43.773 27443-27443/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 70
2021-01-11 06:49:45.320 27443-27443/com.example.myapplication D/MainActivity: ProgressWorker WorkInfo.State is SUCCEEDED
2021-01-11 06:49:45.320 27443-27443/com.example.myapplication D/MainActivity: progressWorkRequest5 value is 0
通过上面的Log,我们可以发现如下两点:
①:在doWork()方法中,每调用一个setProgressAsync()方法,WorkManager的监听中都能监听到一次,并且这个时候State都是RUNNING
②:在调用return Result.success()之前调用setProgressAsync()最好添加一个sleep(),不然这个setProgressAsync()传递出去的值并不能被取到。这个sleep时间我这边测试最小值是20。
取消和停止工作
关于取消就不再次讲解,在前一章的WorkManager的取消中已经进行过相关方法的介绍。
关于停止。
正在运行的Worker可能会由于以下的几种原因而停止运行:
✦明确的取消(cancelWorkXXX)
✦唯一Worker,将ExistingWorkPolicy为REPLACE的新WorkRequest加入到队列中,就的WorkRequest会立即被视为取消
✦工作约束条件不再满足
✦系统出于某种原因指示应用停止Worker。如果超过10分钟的执行期限,可能就会发生这种情况,并且该Worker会在稍后重试。
在工作停止之后,应该释放Worker中保留的所有资源,比如数据库或者文件。
在工作停止的时候,会回调Worker的onStopped() 方法,所以可以在该方法中释放保留的资源。
也可以调用Worker的isStopped() 方法检查Worker是否已经停止。
参考文献: