WorkManager的基本使用

前言

大量应用程序都有在后台执行任务的需求。根据需求的不同,Android为后台任务提供了多种解决方案,如JobScheduler,Loader,Service等。如果这些API没有被适当地使用,可能会消耗大量的电量。Android在解决应用程序耗电问题上做了各种尝试,从Doze到App Standby,通过各种方式限制和管理应用程序,以保证应用程序不会在后台过量消耗设备电量。WorkManager的出现,则是为应用程序中那些不需要及时完成的任务,提供统一的解决方案,以便在设备电量和用户体验之间达到一个比较好的平衡。

兼容范围广

WorkManager最低能兼容API Level 14,并且不需要你的设备安装有Google Play Services。因此,你不用过于担心兼容性问题,因为API Level 14已经能够兼容几乎100%的设备了。

v2-d60e19d93fc05056445cf3cb6a0d16f1_720w
WorkManager依据设备情况选择方案

WorkManager能依据设备的情况,选择不同的执行方案。在API Level 23+,通过JobScheduler来完成任务,而在API Level 23以下的设备中,通过AlarmManagerBroadcast Receivers组合完成任务。但无论采用哪种方案,任务最终都是交由Executor来完成。

WorkManager的两个重要特点

1.针对不需要及时完成的任务

比如,发送应用程序日志,同步应用程序数据,备份用户数据等。站在业务的角度,这些任务都不需要立即完成,如果我们自己来管理这些任务,逻辑可能会非常复杂,若API使用不恰当,可能会消耗大量电量。

2.保证任务一定会被执行

WorkManager能保证任务一定会被执行,即使你的应用程序当前不在运行中,哪怕你的设备重启,任务仍然会在适当的时候被执行。这是因为WorkManager有自己的数据库,关于任务的所有信息和数据都保存在这个数据库中,因此,只要你的任务交给了WorkManager,哪怕你的应用程序彻底退出,或者设备重新启动,WorkManager依然能够保证完成你交给的任务

注意:WorkManager不是一种新的工作线程,它的出现不是为了替代其它类型的工作线程。工作线程通常立即运行,并在执行完成后给到用户反馈。而WorkManager不是即时的,它不能保证任务能立即得到执行。

在项目中使用WorkManager

1.在app的build.gradle中添加依赖。

dependencies {
    def versions = "2.2.0"
    implementation "androidx.work:work-runtime:$versions"
}

2.使用Worker定义任务 。

继承Worker类,覆盖doWork()方法,所有需要在任务中执行的代码都在该方法中编写。

public class UploadLogWorker extends Worker
{
    public UploadLogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams)
    {
        super(context, workerParams);
    }

    /**
     * 耗时的任务,在doWork()方法中执行
     * */
    @NonNull
    @Override
    public Result doWork()
    {
        Log.e("UploadLogWorker", "doWork()");
        return Result.success();
    }
}

doWork()方法有三种类型的返回值:

  • 执行成功返回Result.success()
  • 执行失败返回Result.failure()
  • 需要重新执行返回Result.retry()

3.使用WorkRequest配置任务。通过WorkRequest配置我们的任务何时运行以及如何运行

  • 设置任务触发条件。例如,我们可以设置在设备处于充电,网络已连接,且电池电量充足的状态下,才出发我们设置的任务。
Constraints constraints = new Constraints.Builder()
        .setRequiresCharging(true)
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .build();
  • 将该Constraints设置到WorkRequest。WorkRequest是一个抽象类,它有两种实现,OneTimeWorkRequestPeriodicWorkRequest,分别对应的是一次性任务和周期性任务。
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
        .setConstraints(constraints)//设置触发条件
        .build();
  • 设置延迟执行任务。假设你没有设置触发条件,或者当你设置的触发条件符合系统的执行要求,此时,系统有可能立刻执行该任务,但如果你希望能够延迟执行,那么可以通过setInitialDelay()方法,延后任务的执行。
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
        .setInitialDelay(10, TimeUnit.SECONDS)//符合触发条件后,延迟10秒执行
        .build();
  • 设置指数退避策略。假如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后,你就可以通过该Tag跟踪任务的状态WorkManager.getWorkInfosByTagLiveData(String tag)或者取消任务WorkManager.cancelAllWorkByTag(String tag)。
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
                .addTag("UploadTag")
                .build();

4.将任务提交给系统。WorkManager.enqueue()方法会将你配置好的WorkRequest交给系统来执行。

WorkManager.getInstance(this).enqueue(uploadWorkRequest);

5.观察任务的状态。

任务在提交给系统后,通过WorkInfo获知任务的状态,WorkInfo包含了任务的id,tag,以及Worker对象传递过来的outputData,以及任务当前的状态。有三种方式可以得到WorkInfo对象。

WorkManager.getWorkInfosByTag()

WorkManager.getWorkInfoById()

WorkManager.getWorkInfosForUniqueWork()

如果你希望能够实时获知任务的状态。这三个方法还有对应的LiveData方法。

WorkManager.getWorkInfosByTagLiveData()

WorkManager.getWorkInfoByIdLiveData()

WorkManager.getWorkInfosForUniqueWorkLiveData()

通过LiveData,我们便可以在任务状态发生变化的时候,收到通知。

WorkManager.getInstance(this).getWorkInfoByIdLiveData(uploadWorkRequest.getId()).observe(MainActivity.this, new Observer<WorkInfo>()
{
    @Override
    public void onChanged(WorkInfo workInfo)
    {
        Log.d("onChanged()->", "workInfo:"+workInfo);
    }
});

6.取消任务。与观察任务类似的,我们也可以根据Id或者Tag取消某个任务,或者取消所有任务。

WorkManager.getInstance(MainActivity.this).cancelAllWork();

7.WorkManager和Worker之间的参数传递。数据的传递通过Data对象来完成。

WorkManager通过setInputData()方法向Worker传递数据。

Data inputData = new Data.Builder().putString("input_data", "Hello World!").build();

OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadLogWorker.class)
                .setInputData(inputData)
                .build();

Worker中接收数据,并在任务执行完成后,向WorkManager传递数据。

@Override
public Result doWork()
{
    //接收外面传递进来的数据
    String inputData = getInputData().getString("input_data");

    // 任务执行完成后返回数据
    Data outputData = new Data.Builder().putString("output_data", "Task Success!").build();

    return Result.success(outputData);
}

WorkManager通过LiveDataWorkInfo.getOutputData(),得到从Worker传递过来的数据。

WorkManager.getInstance(this).getWorkInfoByIdLiveData(uploadWorkRequest.getId()).observe(MainActivity.this, new Observer<WorkInfo>()
{
    @Override
    public void onChanged(WorkInfo workInfo)
    {
        if (workInfo != null && workInfo.getState() == WorkInfo.State.SUCCEEDED)
        {
            String outputData = workInfo.getOutputData().getString("output_data");
        }
    }
});

注意:Data只能用于传递一些小的基本类型数据,且数据最大不能超过10kb。

8.周期任务PeriodicWorkRequest。前面提到过,WorkRequest的两种实现OneTimeWorkRequest和PeriodicWorkRequest,分别对应的是一次性任务和周期性任务。一次性任务,即任务在成功完成后,便彻底结束。而周期性任务则会按照设定的时间定期执行。二者使用起来没有太大差别。

需要注意的是:周期性任务的间隔时间不能小于15分钟

PeriodicWorkRequest uploadWorkRequest = new PeriodicWorkRequest.Builder(UploadLogWorker.class, 15, TimeUnit.MINUTES)
        .setConstraints(constraints)
        .addTag(TAG)
        .build();

9.任务链。如果你有一系列的任务需要顺序执行,那么可以利用WorkManager.beginWith().then().then()…enqueue()方法。例如:我们在上传数据之前,需要先对数据进行压缩。

WorkManager.getInstance(this).beginWith(compressWorkRequest).then(uploadWorkRequest).enqueue();

假设在上传数据之前,除了压缩数据,还需要更新本地数据。压缩与更新本地数据二者没有顺序,但与上传数据存在先后顺序。

WorkManager.getInstance(this).beginWith(compressWorkRequest, updateLocalWorkRequest).then(uploadWorkRequest).enqueue();

假设有更复杂的任务链,你还可以考虑使用WorkContinuation.combine()方法,将任务链组合起来。

WorkContinuation workContinuation1 =  WorkManager.getInstance(this).beginWith(WorkRequestA).then(WorkRequestB);
WorkContinuation workContinuation2 =  WorkManager.getInstance(this).beginWith(WorkRequestC).then(WorkRequestD);
List<WorkContinuation> taskList = new ArrayList<>();
taskList.add(workContinuation1);
taskList.add(workContinuation2);
WorkContinuation.combine(taskList).then(WorkRequestE).enqueue();
v2-4d38b482a21ef04e788bebfeb25f87a8_720w
采用WorkContinuation.combine()的任务链执行顺序

总结

开发者经常需要处理后台任务,如果处理后台任务所采用的API没有被正确使用,那么很可能会消耗大量设备的电量。Android出于设备电量的考虑,为开发者提供了WorkManager,旨在将一些不需要及时完成的任务交给它来完成。虽然WorkManager宣称,能够保证任务得到执行,但我在真实设备中,发现应用程序彻底退出与重启设备,任务都没有再次执行。查阅了相关资料,发现这应该与系统有关系。我们前面也提到了,WorkManager会根据系统的版本,决定采用JobScheduler或是AlarmManager+Broadcast Receivers来完成任务。但是这些API很可能会受到OEM系统的影响。比如,假设某个系统不允许AlarmManager自动唤起,那么WorkManager很可能就无法正常使用。

而后我在模拟器中进行测试,模拟器采用的是Google原生系统,发现无论是彻底退出应用程序,或是重启设备,任务都能够被执行。所以,WorkManager在真实设备中不能正常使用,很可能就是系统的问题。因此,开发者在使用WorkManager作为解决方案时,一定要慎重。

另外,我还发现,周期任务的实际执行,与所设定的时间差别较大。执行时间看起来并没有太明显的规律。并且在任务执行完成后,WorkInfo并不会收到Success的通知。查阅了相关资料,发现Android认为Success和Failure都属于终止类的通知。意思是,如果发出这类通知,则表明任务彻底结束,而周期任务不会彻底终止,会一直执行下去,所以我们在使用LiveData观察周期任务时,不会收到Success这类的通知。这也是我们需要注意的地方。

WorkManager的基本使用
滚动到顶部