Jetpack组件之WorkManager

Jetpack组件之WorkManager

Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager

首语

Android应用中大部分都需要执行后台任务,因此也提供了多种解决方案,如JobSchedulerLoader等。但不合理的使用这些API,会造成消耗大量电量。JetPack中的WorkManager为应用程序执行后台任务提供了 一个统一的解决方案。
WorkManager可以自动维护后台任务的执行时机,执行顺序,执行状态。

特点

  1. 兼容范围广
    WorkManager最低能兼容API Level 14,并且不需要你的设备安装Google Play Services。因此,不必担心兼容性的问题。
    WorkManager能根据设备的情况,选择不同的执行方案。在API Level23以上的设备中,通过JobScheduler完成任务;在API Level23以下的设备中,通过AlarmManagerBroadcast Receiver组合来执行任务,无论哪种方案,任务最终都是交给Executor来执行的。
    注: WorkManager并不是一种新的工作线程,工作线程通常立即执行,而WorkManager不能保证任务被及时执行。
  2. 任务一定会被执行
    WorkManager能保证任务一定会被执行,即使应用程序不在运行中,甚至是在设备重启后,任务仍然会在适当的时刻执行,这是因为WorkManager有自己的数据库,任务的所有信息和数据都保存在数据库中。
    注: WorkManager宣称能够保证任务得到执行,但是在非Android原生系统的真是设备上进行测试发下,应用彻底退出和设备重启后,任务没有再次被执行。这也是Android的碎片化所导致的,许多厂商修改了手机ROM,造成不能得到执行,因此,分析需求是否可以使用WorkManager。
  3. 针对不需要及时完成的任务
    例如,发送应用日志、同步应用数据,备份应用数据等。这些任务不需要立刻被执行。

依赖

dependencies {
  def work_version = "2.5.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"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
  }

任务构建

项目开发中有这样的需求;任务A执行完再执行C,任务B执行完再执行D;任务A、B、C、D都执行完成后再执行E。那么可以利用WorkManager.beginWith().then().then...enqueue()的方式构建任务链。

WorkContinuation left,right;
left=WorkManager.getInstance(this).beginWith(A).then(C);
right=WorkManager.getInstance(this).beginWith(B).then(D);
WorkContinuation.combine(Arrays.asList(left,right)).then(E).enqueue();

任务构建

状态通知

WorkManager的每个任务都有以下几种状态:
BLOCKED表示任务被阻塞;ENQUEUED表示刚加入执行队列;RUNNING表示任务正在被执行,其中SUCCESSEDCANCELEDFAILED表示任务执行成功、取消执行和执行失败;FINISHED表示任务结束。

任务控制

        //通过ID取消单个任务
        WorkManager.getInstance(this).cancelWorkById(UUID);
        //通过tag取消所有任务
        WorkManager.getInstance(this).cancelAllWorkByTag(tag);
        //通过任务名取消唯一任务
        WorkManager.getInstance(this).cancelUniqueWork(workName);
        //取消所有任务
        WorkManager.getInstance(this).cancelAllWork();

类关系

  1. Worker
    任务的执行者,是一个抽象类,需要继承它实现要执行的任务。
  2. WorkRequest
    指定让哪个Woker执行任务,指定执行的环境,执行的顺序等。要使用它的子类OneTimeWorkRequestPeriodicWorkRequest
  3. WokManager
    管理任务请求和任务队列,发起的WorkRequest会进入它的任务队列。
  4. WorkStatus
    包含有任务的状态和任务的信息,以LiveData的形式提供给观察者。

使用

创建任务

使用Worker类定义任务,复写doWork(),在doWork()里执行耗时任务。

public class UploadFileWorker extends Worker {
    public UploadFileWorker(@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();
    }
}

输入参数

任务执行过程中可能需要传递一些参数,通过{key,value}的形式添加到Data中。

    @Override
    public Result doWork() {
        //接收从外面传递进来的数据
        Data inputData = getInputData();
        String filePath = inputData.getString("file");
        String fileUrl = FileUploadManager.upload(filePath);
        //任务完成后返回数据
        if (TextUtils.isEmpty(fileUrl)) {
            return Result.failure();
        } else {
            Data outputData = new Data.Builder().putString("fileUrl", fileUrl)
                    .build();
            return Result.success(outputData);
        }
    }

需要注意的是,在Data源码中发现,Data传递的数据最大不能超过10KB,因此Data只能传递一些小的基本类型的数据。

   /**
     * The maximum number of bytes for Data when it is serialized (converted to a byte array).
     * Please see the class-level Javadoc for more information.
     */
    public static final int MAX_DATA_BYTES = 10 * 1024;    // 10KB

使用WorkRequest配置任务

WorkRequest 本身是抽象基类。该类有两个实现类,可用于创建 OneTimeWorkRequestPeriodicWorkRequest 请求。OneTimeWorkRequest 适用于调度非重复性工作,而 PeriodicWorkRequest 则更适合调度以一定间隔重复执行的工作。

  • OneTimeWorkRequest
    对于无需额外配置的简单工作,请使用静态方法 from。
WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);

对于比较复杂的任务,可以使用构建器。

 WorkRequest uploadWorkRequest =
        new OneTimeWorkRequest.Builder(MyWork.class)
            // Additional configuration
            .build();
  • PeriodicWorkRequest
    应用有时可能需要定期运行某些工作。例如,需要定期备份数据、或者定期上传日志到服务器。
 //任务的运行时间间隔定为一小时
     PeriodicWorkRequest saveRequest =
            new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS)
                // Constraints
                .build();

时间间隔定义为两次重复执行之间的最短时间。工作器的确切执行时间取决于您在 WorkRequest 对象中设置的约束以及系统执行的优化。
需要注意的是定义的最短重复间隔是 15 分钟,在源码中也可以看到。

/**
  * The minimum interval duration for {@link PeriodicWorkRequest} (in milliseconds).
  */
public static final long MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L; // 15 minutes.

如果对任务的执行实际比较敏感,可以将PeriodicWorkRequest 配置为在每个时间间隔的灵活时间段内执行。

  //每小时的最后15分钟内执行的定期任务
     WorkRequest saveRequest =
            new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class,
                    1, TimeUnit.HOURS,
                    15, TimeUnit.MINUTES)
                .build();

定义的最短灵活间隔是5分钟,在源码中也可以看到。

/**
  * The minimum flex duration for {@link PeriodicWorkRequest} (in milliseconds).
  */
public static final long MIN_PERIODIC_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes.

在执行任务之前,我们可以对任务添加各种约束,使其满足约束条件后才执行任务。通过Contraints.Builder()创建Constraints 实例。

  Constraints constraints = new Constraints.Builder()
                //设备存储空间充足的时候 才能执行 ,>15%
                .setRequiresStorageNotLow(true)
                //必须在执行的网络条件下才能好执行,不计流量 ,wifi
                .setRequiredNetworkType(NetworkType.UNMETERED)
                //设备的充电量充足的才能执行 >15%
                .setRequiresBatteryNotLow(true)
                //只有设备在充电的情况下 才能允许执行
                .setRequiresCharging(true)
                //只有设备在空闲的情况下才能被执行 比如息屏,cpu利用率不高
                .setRequiresDeviceIdle(true)
                //workmanager利用contentObserver监控传递进来的这个uri对应的内容是否发生变化,当且仅当它发生变化了
                //设置从content变化到被执行中间的延迟时间,如果在这期间。content发生了变化,延迟时间会被重新计算
                .setTriggerContentUpdateDelay(Duration.ZERO)
                //设置从content变化到被执行中间的最大延迟时间
                .setTriggerContentMaxDelay(Duration.ZERO).build();

再将Constraint实例设置到OneTimeWorkRequest中去。

OneTimeWorkRequest request = new OneTimeWorkRequest
                .Builder(UploadFileWorker.class)
                .setInputData(inputData)
                .setConstraints(constraints)
                //对任务添加标记
                .addTag("tag")
    			//设置延迟执行时间
                .setInitialDelay(10,TimeUnit.SECONDS)
                //设置一个拦截器,在任务执行之前 可以做一次拦截,去修改入参的数据然后返回新的数据交由worker使用
                .setInputMerger(null)
                //当一个任务被调度失败后,所要采取的重试策略,可以通过BackoffPolicy来执行具体的策略
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
                //任务被调度执行的延迟时间
                .setInitialDelay(10, TimeUnit.SECONDS)
                //设置该任务尝试执行的最大次数
                .setInitialRunAttemptCount(2)
                //设置这个任务开始执行的时间
                //System.currentTimeMillis()
                .setPeriodStartTime(0, TimeUnit.SECONDS)
                //指定该任务被调度的时间
                .setScheduleRequestedAt(0, TimeUnit.SECONDS)
                //当一个任务执行状态变成finish时,又没有后续的观察者来消费这个结果,那么workmanager会在
                //内存中保留一段时间的该任务的结果。超过这个时间,这个结果就会被存储到数据库中
                //下次想要查询该任务的结果时,会触发workmanager的数据库查询操作,可以通过uuid来查询任务的状态
                .keepResultsForAtLeast(10, TimeUnit.SECONDS)
                .build();
fileUploadUUID = request.getId();

将任务提交给系统

WorkContinuation workContinuation = WorkManager.getInstance(this).beginWith(workRequests);
workContinuation.enqueue();

监听执行状态和结果

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

 WorkManager.getInstance(this).getWorkInfoById(UUID);
 WorkManager.getInstance(this).getWorkInfosByTag(tag);
 WorkManager.getInstance(this).getWorkInfosForUniqueWork(workName);

实时获取任务的状态也有三种方法。

 WorkManager.getInstance(this).getWorkInfoByIdLiveData(UUID);
 WorkManager.getInstance(this).getWorkInfosByTag(tag);
 WorkManager.getInstance(this).getWorkInfosForUniqueWorkLiveData(workName);

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

 WorkManager.getInstance(this).getWorkInfoByIdLiveData(fileUploadUUID).observe(this, new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {
                WorkInfo.State state = workInfo.getState();
                if (state == WorkInfo.State.FAILED) {
                    //失败
                } else if (state == WorkInfo.State.SUCCEEDED) {
                    //成功
                }
                workInfo.getProgress();
                workInfo.getId();

            }
 });
 workContinuation.getWorkInfosLiveData().observe(PublishActivity.this, new Observer<List<WorkInfo>>() {
            @Override
            public void onChanged(List<WorkInfo> workInfos) {
             for (WorkInfo workInfo : workInfos) {	
                    WorkInfo.State state = workInfo.getState();
                    Data outputData = workInfo.getOutputData();
                    UUID uuid = workInfo.getId();
             }
 });

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.yanghujun.com/archives/workmanager

Buy me a cup of coffee ☕.