WorkManager是为了那些可延后执行的任务而设计,这些任务不需要立即执行,但是需要保证任务能被执行,即使应用退出或者设备重启。例如:
WorkManager不是为某些进程内的后台任务设计的,这些任务会在app进程退出时被停止,也不是那些需要立即执行的任务。
dependencies { def work_version = "2.2.0" // (Java only) implementation "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" // optional - RxJava2 support 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" } 复制代码
继承Worker,并重写doWork()
public class UploadWorker extends Worker { public UploadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @NonNull @Override public Result doWork() { //business logic return Result.success(); } } 复制代码
Result返回结果有三种:
Worker定义了具体的任务,WorkRequest定义了如何执行以及何时执行任务。如果是一次性的任务,可以用O呢TimeWorkRequest,如果是周期性的任务,可以使用PeriodicWorkRequest。
OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).build(); 复制代码
PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(UploadWorker.class, 10, TimeUnit.SECONDS).build(); 复制代码
调用WorkManager的enqueue方法
WorkManager.getInstance(ctx).enqueue(uploadReq);
复制代码
任务的具体执行时机依赖于WorkRequest设置的约束,以及系统的优化。
通过自定义WorkRequest可以解决以下场景:
给任务增加约束,表示什么时候该任务能执行。
例如,可以指定任务只有在设备空闲或者连接到电源时才能执行。
Constraints constraints = new Constraints.Builder() //当本地的contenturi更新时,会触发任务执行(api需大于等于24,配合JobSchedule) .addContentUriTrigger(Uri.EMPTY, true) //当content uri变更时,执行任务的最大延迟,配合JobSchedule .setTriggerContentMaxDelay(10, TimeUnit.SECONDS) //当content uri更新时,执行任务的延迟(api>=26) .setTriggerContentUpdateDelay(100, TimeUnit.SECONDS) //任务的网络状态:无网络要求,有网络连接,不限量网络,非移动网络,按流量计费的网络 .setRequiredNetworkType(NetworkType.NOT_ROAMING) //电量足够才能执行 .setRequiresBatteryNotLow(true) //充电时才能执行 .setRequiresCharging(false) //存储空间足够才能执行 .setRequiresStorageNotLow(false) //设备空闲才能执行 .setRequiresDeviceIdle(true) .build(); 复制代码
当设置了多个约束,只有这些条件都满足时,任务才会执行。
当任务在运行时,如果约束条件不满足,WorkManager会终止任务。这些任务会在下一次约束条件满足时重试。
如果任务没有约束或者约束条件满足时,系统可能会立刻执行这些任务。如果不希望任务立即执行,可以指定这些任务延迟一定时间再执行。
OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).setInitialDelay(10, TimeUnit.SECONDS).build(); 复制代码
如果需要WorkManager重试任务,可以让任务返回Result.retry()。
任务会被重新调度,并且会有一个默认的补偿延迟和策略。补偿延迟指定了任务被重试的一个最小的等待时间。补充策略定义了补偿延迟在接下来的几次重试中会如何增加。默认是指数增加的。
OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).setInitialDelay(10, TimeUnit.SECONDS) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10 ,TimeUnit.SECONDS) .build(); 复制代码
任务可能需要传入数据作为输入参数或者返回结果数据。例如,一个上传图片的任务需要图片的URI,可能也需要图片上传后的地址。
输入和输出的值以键-值对的形式存储在Data对象中。
Data data = new Data.Builder().putString("key1", "a").build(); OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class) .setInputData(data) .build(); 复制代码
Wroker类调用Worker.getInputData()来获取输入参数。
Data类也可以作为输出。在Worker中返回Data对象,通过调用Result.success(data)或Result.failure(data)。
public class UploadWorker extends Worker { public UploadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @NonNull @Override public Result doWork() { //business logic Data data = new Data.Builder().putString("image-url","http://xxxx.png").build(); return Result.success(data); } } 复制代码
对任何的WorkRequest对象,通过给一组任务赋值一个标签就可以在逻辑上把它们变成一个组。这样就可以操作特定标签的全部任务。
例如,WorkManager.cancelAllWorkByTag(String)取消了所有该标签的任务;WorkManager.getWorkInfosByTagLiveData(String)返回了一个LiveData包含了该标签下的全部任务的状态列表
OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class) .addTag("upload") .build(); 复制代码
在任务的生命周期中,会经过各种状态:
当把任务放入队列中,WorkManager允许检查它们的状态。这些信息可以通过WorkInfo对象获取,包含了任务的id,tag,当前的State和输出的数据。
有以下几个方法获取WorkInfo:
上述方法返回的LiveData可以通过注册一个监听器观察WorkInfo的变化。
WorkInfo workInfo = WorkManager.getInstance(this).getWorkInfoById(UUID.fromString("uuid")).get(); WorkManager.getInstance(this).getWorkInfoByIdLiveData(UUID.fromString("uuid")).observe(this, new Observer<WorkInfo>() { @Override public void onChanged(WorkInfo workInfo) { } }); 复制代码
2.3.0-alpha01版本的WorkManager增加了设置和观察任务的进度的支持。如果应用在前台时任务在运行,进度信息可以展示给用户,通过API返回的WorkInfo的LiveData。
ListenableWorker现在支持setProgressAsync(),能够保存中间进度。这些API使得开发者能够设置进度,以便在UI上能够展示出来。进度用Data类型表示,这是一个可序列化的属性容器(类似输入和输出,受同样的限制)。
进度信息只有在ListenableWorker运行时才能被观察和更新。当ListenableWorker结束时设置进度会被忽略。通过调用getWorkInfoBy..()或者getWorkInfoBy...LiveData()接口来观察进度信息。这些方法能返回WorkInfo的对象实例,它们有一个新的getProgress()方法能返回Data对象。
开发者使用ListenableWorker或者Worker,setProgressAsync()接口会返回一个ListenableFuture;更新进度是异步的,包含了存储进度信息到数据库。在Kotlin中,可以使用CoroutineWorker对象的setProgress()扩展方法来更新进度信息。
public class ProgressWorker extends Worker { public ProgressWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); setProgressAsync(new Data.Builder().putInt("progress", 0).build()); } @NonNull @Override public Result doWork() { setProgressAsync(new Data.Builder().putInt("progress", 100).build()); return Result.success(); } } 复制代码
观察进度信息比较简单。可以使用getWorkInfoBy...()或getWorkInfoBy...LiveData()方法,获取一个WorkInfo的引用。
WorkRequest progress = new OneTimeWorkRequest.Builder(ProgressWorker.class).addTag("progress").build(); WorkManager.getInstance(this).getWorkInfoByIdLiveData(progress.getId()).observe(this, new Observer<WorkInfo>() { @Override public void onChanged(WorkInfo workInfo) { int progress = workInfo.getProgress().getInt("progress", 0); } }); 复制代码
WorkManager允许创建和入队一连串的任务,可以指定多个依赖的任务,以及它们的执行顺序。如果要以一个特定的顺序执行多个任务时会非常有用。
要创建一连串的任务,可以使用WorkManager.beginWith(OneTimeWorkRequest)或者WorkManager.beginWith(List),它们会返回一个WorkContinuation实例。
一个WorkContinuation实例之后可以用来添加依赖的OneTimeWorkRequest,通过调用WorkContainuation.then(OneTimeWorkRequest)或WorkContinuation.then(List)。
每个WorkContinuation.then(...)的调用,会返回一个新的WorkContinuation实例。如果添加了OneTimeRequest的列表,这些请求有可能会串行地运行。
最终,可以用WorkContinuation.enqueue()方法把WorkContinuation链放入队列。
WorkManager.getInstance(myContext) // Candidates to run in parallel .beginWith(Arrays.asList(filter1, filter2, filter3)) // Dependent work (only runs after all previous work in chain) .then(compress) .then(upload) // Don't forget to enqueue() .enqueue(); 复制代码
当使用链式的OneTimeWorkRequest,父OneTimeWorkRequest的输出会作为子任务的输入。所以上例中的filter1,filter2和filter3的输出会作为compress任务的输入。
为了管理来自多个父任务的输入,WorkManager使用InputMerger进行输入合并。
WorkManager提供了两种不同类型的InputMerger:
OneTimeWorkRequest compress = new OneTimeWorkRequest.Builder(CompressWorker.class) .setInputMerger(ArrayCreatingInputMerger.class) .setConstraints(constraints) .build(); 复制代码
当创建一个OneTimeWorkRequest任务链时,有几件事要记住:
如果不再需要入队的任务执行,可以取消它。取消一个单独的WorkRequest最简单的方法是使用id并调用WorkManager.cancenWorkById(UUID)。
WorkManager.cancelWorkById(workRequest.getId());
复制代码
在底层,WorkManager会检查任务的状态。如果这个任务已经完成,没有任何事情发生。否则,这个任务的状态会转移到CANCELED 并且这个任务以后不会再运行。任何依赖这个任务的其他WorkRequest都会被标记为CANCELED。
另外,如果当前任务正在运行,这个任务会触发ListenableWorker.onStopped()的回调。重写这个方法来处理任何可能的清理工作。
也可以用标签来取消任务,通过调用WorkManager.cancelAllWorkByTag(String)。注意,这个方法会取消所有有这个标签的任务。另外,也可以调用WorkManager.cancelUniqueWork(String)取消带有该独特名字的全部任务。
有几种情况,运行中的任务会被WorkManager终止:
在这些情况下,任务会触发ListenableWorker.onStopped()的回调。你应该执行任务清理和配合地终止任务,以防系统会关闭应用。比如,在此时应该关闭开启的数据库和文件句柄,或者在更早的时间里做这些事情。另外,无论何时想要判断任务是否被终止了可以查询ListenableWorker.isStopped()。即使您通过在调用onStopped()之后返回一个结果来表示您的工作已经完成,WorkManager也会忽略这个结果,因为这个任务已经被认为是结束了。
你的应用优势会需要周期性地运行某些任务。比如,应用可能会周期性地备份数据,下载新的数据,或者上传到日志到服务器。
使用PeriodicWorkRequest来执行那些需要周期性地运行的任务。
PeriodicWorkRequest不能被链接。如果任务需要链接,考虑使用OneTimeWorkRequest。
Constraints constraints = new Constraints.Builder() .setRequiresCharging(true) .build(); PeriodicWorkRequest saveRequest = new PeriodicWorkRequest.Builder(SaveImageFileWorker.class, 1, TimeUnit.HOURS) .setConstraints(constraints) .build(); WorkManager.getInstance(myContext) .enqueue(saveRequest); 复制代码
周期间隔是两次重复执行的最小时间。任务实际执行的时间取决于任务设置的约束和系统的优化。
观察PeriodicWorkRequest的状态的方法跟OneTimeWorkRequest一样。
唯一任务是一个有用的概念,它保证了某一时刻只能有一个带有特定名称的任务链。不像id是由WorkManager自动生成的,唯一名称是可读的,并且是开发者指定的。也不像tag,唯一名称只能跟一个任务链关联。
可以通过调用WorkManager.enqueueUniqueWork()或者WorkManager.enqueueUniqueWork()来创建一个唯一任务队列。第一个参数是唯一名字—用于识别WorkRequest。第二个参数是冲突的解决策略,指定了如果已经存在一个未完成的同名任务链时WorkManager采取的措施:
如果有一个任务不需要多次放入队列时,唯一任务会很有用。例如,如果你的应用需要同步数据到网络,可以入队一个命名为“sync”的事件,并且如果已经有这个名字的任务了,那么新的任务应该被忽略。如果你需要逐渐地建立一个很长的任务链,唯一任务队列也很有用。例如,一个相片编辑应用可能会让用户撤销一长串编辑动作。每个撤销操作可能会耗时一段时间,但是它们必须按正确的顺序执行。在这个情况下,这个应用可以创建一个“undo”的任务链,并把每个新的操作放在最后。
如果要创建一个唯一任务链,可以使用WorkManager.beginUniqueWork()而不是beginWith()。
WorkManager提供了work-test工件在Android设备上为任务进行单元测试。
为了使用work-test工件,需要在build.gradle中添加androidTestImplementation依赖。
androidTestImplementation "androidx.work:work-testing:2.3.0-alpha01" 复制代码
work-testing提供了一个测试模式下的WorkManager的特殊实现,它是用WorkManagerTestInitHelper进行初始化。
work-testing工件提供了一个SynchronousExecutor使得能更简单地用同步方式进行测试,不需要去处理多线程,锁或占用。
在build.gradle中编辑依赖
testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation "androidx.work:work-testing:2.2.0" 复制代码
单元测试类setup
@Before public void setup() { Context context = ApplicationProvider.getApplicationContext(); Configuration config = new Configuration.Builder() // Set log level to Log.DEBUG to // make it easier to see why tests failed .setMinimumLoggingLevel(Log.DEBUG) // Use a SynchronousExecutor to make it easier to write tests .setExecutor(new SynchronousExecutor()) .build(); // Initialize WorkManager for instrumentation tests. WorkManagerTestInitHelper.initializeTestWorkManager(context, config); } 复制代码
WorkManager在测试模式下已经初始化,可以开始测试任务。
public class TestWorker extends Worker { public TestWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @NonNull @Override public Result doWork() { Data input = getInputData(); if (input.size() == 0) { return Result.failure(); } else { return Result.success(input); } } } 复制代码
测试模式下的使用跟正常应用中使用十分类似。
package com.example.hero.workmgr; import android.content.Context; import android.util.Log; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.work.Configuration; import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; import androidx.work.testing.SynchronousExecutor; import androidx.work.testing.WorkManagerTestInitHelper; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; /** * Instrumented test, which will execute on an Android device. * * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Before public void setup() { Context context = ApplicationProvider.getApplicationContext(); Configuration config = new Configuration.Builder() // Set log level to Log.DEBUG to // make it easier to see why tests failed .setMinimumLoggingLevel(Log.DEBUG) // Use a SynchronousExecutor to make it easier to write tests .setExecutor(new SynchronousExecutor()) .build(); // Initialize WorkManager for instrumentation tests. WorkManagerTestInitHelper.initializeTestWorkManager(context, config); } @Test public void testWorker() throws Exception { Data input = new Data.Builder().put("a", 1).put("b", 2).build(); OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(TestWorker.class).setInputData(input).build(); WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext()); mgr.enqueue(request).getResult().get(); //该接口其实得到的是一个StatusRunnable,从数据库中查询到WorkInfo后会调用SettableFuture.set(),然后get()会返回对应的WorkInfo WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED)); workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING)); workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.SUCCEEDED)); Data output = workInfo.getOutputData(); assertThat(output.getInt("a", -1), is(1)); } } 复制代码
WorkManagerTestInitHelper提供一个TestDriver实例,它能够模拟初始化延迟,ListenableWorker需要的约束条件和循环任务的周期等。
任务可以设置初始化延迟。用TestDriver设置任务所需要的初始化延迟,就不需要等待这个时间到来,这样可以测试任务的延迟是否有效。
@Test public void testDelay() throws Exception { Data input = new Data.Builder().put("a", 1).put("b", 2).build(); OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(TestWorker.class).setInputData(input).setInitialDelay(10, TimeUnit.SECONDS).build(); WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext()); TestDriver driver = WorkManagerTestInitHelper.getTestDriver(ApplicationProvider.getApplicationContext()); mgr.enqueue(request).getResult().get(); driver.setInitialDelayMet(request.getId()); WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED)); workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING)); workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.SUCCEEDED)); } 复制代码
TestDriver可以调用setAllConstraintsMet设置所有的约束都满足条件。
@Test public void testConstraint() throws Exception { Data input = new Data.Builder().put("a", 1).put("b", 2).build(); Constraints constraints = new Constraints.Builder().setRequiresDeviceIdle(true).build(); OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(TestWorker.class).setInputData(input).setConstraints(constraints).build(); WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext()); TestDriver driver = WorkManagerTestInitHelper.getTestDriver(ApplicationProvider.getApplicationContext()); mgr.enqueue(request).getResult().get(); driver.setAllConstraintsMet(request.getId()); WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED)); workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING)); workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.SUCCEEDED)); } 复制代码
TestDriver提供了一个setPeriodDelayMet来表示间隔已经达到。
@Test public void testPeriod() throws Exception { Data input = new Data.Builder().put("a", 1).put("b", 2).build(); PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(TestWorker.class, 10, TimeUnit.SECONDS).setInputData(input).build(); WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext()); TestDriver driver = WorkManagerTestInitHelper.getTestDriver(ApplicationProvider.getApplicationContext()); mgr.enqueue(request).getResult().get(); driver.setPeriodDelayMet(request.getId()); WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED)); workInfo = mgr.getWorkInfoById(request.getId()).get(); assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING)); workInfo = mgr.getWorkInfoById(request.getId()).get(); //循环任务完成后,状态仍会变成ENQUEUED(WorkerWrapper中的handleResult()的逻辑) assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED)); } 复制代码
从2.1.0版本开始,WorkManager提供了新的API,能更方便的测试Worker,ListenableWorker,以及ListenableWorker的变体(CoroutineWorker 和RxWorker)。
之前,为了测试任务,需要使用WorkManagerTestInitHelper来初始化WorkManager。在2.1.0中,不一定要使用它。如果只是为了测试任务中的业务逻辑,再也不需要使用WorkManagerTestInitHelper。
为了测试ListenableWorker和它的变体,可以使用TestListenableWorkerBuilder。这个建造器可以创建一个ListenableWorker的实例,用来测试任务中的业务逻辑。
package com.example.hero.workmgr; import android.content.Context; import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; import androidx.concurrent.futures.ResolvableFuture; import androidx.work.ListenableWorker; import androidx.work.WorkerParameters; import com.google.common.util.concurrent.ListenableFuture; public class SleepWorker extends ListenableWorker { private ResolvableFuture<Result> mResult; private Handler mHandler; private final Object mLock; private Runnable mRunnable; public SleepWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) { super(appContext, workerParams); mResult = ResolvableFuture.create(); mHandler = new Handler(Looper.getMainLooper()); mLock = new Object(); } @NonNull @Override public ListenableFuture<Result> startWork() { mRunnable = new Runnable() { @Override public void run() { synchronized (mLock) { mResult.set(Result.success()); } } }; mHandler.postDelayed(mRunnable, 1000L); return mResult; } @Override public void onStopped() { super.onStopped(); if (mRunnable != null) { mHandler.removeCallbacks(mRunnable); } synchronized (mLock) { if (!mResult.isDone()) { mResult.set(Result.failure()); } } } } 复制代码
为了测试SleepWorker,先用TestListenableWorkerBuilder创建了一个Worker的实例。这个创建器也可以用来设置标签,输入和尝试运行次数等参数。
@Test public void testSleepWorker() throws Exception{ //直接创建了一个worker实例,调用它的方法 ListenableWorker worker = TestListenableWorkerBuilder.from(ApplicationProvider.getApplicationContext(), SleepWorker.class).build(); ListenableWorker.Result result = worker.startWork().get(); assertThat(result, is(ListenableWorker.Result.success())); } 复制代码
有一个任务如下:
public class Sleep extends Worker { public Sleep(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @NonNull @Override public Result doWork() { try { Thread.sleep(2000); }catch (Exception e){ e.printStackTrace(); } return Result.success(); } } 复制代码
使用TestWorkerBuilder进行测试。TestWorkerBuilder允许指定运行任务的线程池。
@Test public void testThreadSleepWorker() throws Exception { Sleep woker = (Sleep) TestWorkerBuilder.from(ApplicationProvider.getApplicationContext(), Sleep.class, Executors.newSingleThreadExecutor()).build(); ListenableWorker.Result result = woker.doWork(); assertThat(result, is(ListenableWorker.Result.success())); } 复制代码