Android Jetpack架构组件(七)之WorkManager

439

一、WorkManager概述

1.1 WorkManager简介

在Android应用开发中,或多或少的会有后台任务的需求,根据需求场景的不同,Android为后台任务提供了多种不同的解决方案,如Service、Loader、JobScheduler和AlarmManger等。后台任务通常用在不需要用户感知的功能,并且后台任务执行完成后需要即时关闭任务回收资源,如果没有合理的使用这些API就会造成电量的大量消耗。为了解决Android电量大量消耗的问题,Android官方做了各种优化尝试,从Doze到app Standby,通过添加各种限制和管理应用程序进程来包装应用程序不会大量的消耗电量。

为了解决Android耗电的问题,Android提供了WorkManager ,用来对应用中那些不需要及时完成的任务提供一个统一的解决方案,借助WorkManager,开发者可以轻松调度那些即使在退出应用或重启设备时仍应运行的可延期异步任务。WorkManager是一套AP,用来替换先前的 Android 后台调度 API(包括 FirebaseJobDispatcher、GcmNetworkManager 和 JobScheduler)等组件。WorkManager需要API级别为14,同时可保证电池续航时间。

WorkManager的兼容性体现在能够根据系统版本,选择不同的方案来实现,在API低于23时,采用AlarmManager+Broadcast Receiver,高于23时采用JobScheduler。但无论采用哪种方式,任务最终都是交由Executor来执行。下图展示了WorkManager底层作业调度服务的运作流程。 在这里插入图片描述 需要注意的是,WorkManager不是一种新的工作线程,它的出现不是为了替换其他类型的工作线程。工作线程通常能够立即执行,并在任务完成后将结果反馈给用户,而WorkManager不是即时的,它不能保证任务能够被立即执行。

1.2 WorkManager特点

WorkManager有以下三个特点:

  • 用来实现不需要即时完成的任务,如后台下载开屏广告、上传日志信息等;
  • 能够保证任务一定会被执行;
  • 兼容性强。

针对不需要即时完成的任务

在Android开发中,经常会遇到后台下载、上传日志信息等需求,一般来说,这些任务是不需要立即完成的,如果我们自己使用来管理这些任务,逻辑可能会非常负责,并且如果处理不恰当会造成大量的电量消耗。

后台延时任务

WorkManager能够保证任务一定会被执行,但不是不能保证被立即执行,也即说在适当的时候被执行。因为WorkManager有自己的数据库,与任务相关的信息和数据就保存到数据库中。所以,只要任务已经提交到WorkManager,即使应用推出或者设备重启也不需要担心任务被丢失。

兼容性广

WorkManager能够兼容API 14,并且不需要你的设备安装Google Play Services,因此不用担心出现兼容性问题。

除此之外,WorkManager 还具备许多其他关键优势。

工作约束

使用工作约束明确定义工作运行的最佳条件。例如,仅在设备采用 Wi-Fi 网络连接时、当设备处于空闲状态或者有足够的存储空间时再运行。

强大的调度

WorkManager 允许开发者使用灵活的调度窗口调度工作,以运行一次性或重复工作。还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。已调度的工作存储在内部托管的 SQLite 数据库中,由 WorkManager 负责确保该工作持续进行,并在设备重新启动后重新调度。此外,WorkManager 遵循低电耗模式等省电功能和最佳做法,因此开发者无需考虑电量消耗的问题。

灵活的重试政策

有时任务执行会出现失败,WorkManager 提供了灵活的重试政策,包括可配置的指数退避政策。

工作链接

对于复杂的相关工作,我们可以使用流畅自然的界面将各个工作任务链接在一起,这样便可以控制哪些部分依序运行,哪些部分并行运行,如下所示。

WorkManager.getInstance(...)
    .beginWith(Arrays.asList(workA, workB))
    .then(workC)
    .enqueue();

内置线程互操作性

WorkManager 无缝集成 RxJava 和 协程,灵活地插入您自己的异步 API。

1.3 WorkManager的几个概念

使用WorkManager时有几个重要的概念需要注意。

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

二、基本使用

2.1 添加依赖

如需开始使用 WorkManager,请先将库导入您的 Android 项目中。

dependencies {
  def work_version = "2.4.0"
  implementation "androidx.work:work-runtime:$work_version"
}

添加依赖项并同步 Gradle 项目后。

2.2 定义 Worker

创建一个继承自Worker的Worker类,然后在Worker类的doWork()方法中执行要运行的任务,并且需要返回任务状态的结果。例如,在doWork()方法实现上传图像的 任务。

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

   @Override
   public Result doWork() {
     // Do the work here--in this case, upload the images.
     uploadImages();
     return Result.success();
   }
}

在doWork()方法中执行的任务最终需要返回一个Result类型对象,表示任务执行结果,有三个枚举值。

  • Result.success():工作成功完成。
  • Result.failure():工作失败。
  • Result.retry():工作失败,根据其重试政策在其他时间尝试。

2.3 创建 WorkRequest

完成Worker的定义后,必须使用 WorkManager 服务进行调度该工作才能运行。对于如何调度工作,WorkManager 提供了很大的灵活性。开发者可以将其安排为在某段时间内定期运行,也可以将其安排为仅运行一次。

不论您选择以何种方式调度工作,请使用 WorkRequest执行任务的请求。Worker 定义工作单元,WorkRequest(及其子类)则定义工作运行方式和时间,如下所示。

WorkRequest uploadWorkRequest =
   new OneTimeWorkRequest.Builder(UploadWorker.class)
       .build();

然后,使用 WorkManager的enqueue() 方法将 WorkRequest 提交到 WorkManager,如下所示。

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

执行工作器的确切时间取决于 WorkRequest 中使用的约束和系统优化方式。

三、方法指南

3.1 WorkRequest

3.1.1 WorkRequest概览

WorkRequest主要用于向Worker提交任务请求,我们可以使用WorkRequest来处理以下一些常见的场景。

  • 调度一次性工作和重复性工作
  • 设置工作约束条件,例如要求连接到 Wi-Fi 网络或正在充电才会执行WorkRequest
  • 确保至少延迟一定时间再执行工作
  • 设置重试和退避策略
  • 将输入数据传递给工作
  • 使用标记将相关工作分组在一起

WorkRequest是一个抽象类,它有两个子类,分别是OneTimeWorkRequest和PeriodicWorkRequest,前者实现只执行一次的任务,后者用来实现周期性任务。

3.1.2 一次性任务

如果任务只需要执行一次,那么可以使用WorkRequest的子类OneTimeWorkRequest。对于无需额外配置的简单工作,可以使用OneTimeWorkRequest类的静态方法 from(),如下所示。

WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);

对于更复杂的工作,则可以使用构建器的方式来创建WorkRequest,如下所示。

WorkRequest uploadWorkRequest =
   new OneTimeWorkRequest.Builder(MyWork.class)
       .build();

3.1.3 定期任务

如果需要定期运行某些工作,那么可以使用PeriodicWorkRequest。例如,可能需要定期备份数据、定期下载应用中的新鲜内容或者定期上传日志到服务器等。

PeriodicWorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS)
           .build();

上面的代码定义了一个运行时间间隔定为一小时的定期任务。不过,工作器的确切执行时间取决于您在 WorkRequest 对象中设置的约束以及系统执行的优化。

如果任务的性质对运行的时间比较敏感,可以将 PeriodicWorkRequest 配置为在每个时间间隔的灵活时间段内运行,如图 1 所示。 在这里插入图片描述

如需定义具有灵活时间段的定期工作,请在创建 PeriodicWorkRequest 时传递 flexInterval和 repeatInterval两个参数,如下所示。

WorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class,
               1, TimeUnit.HOURS,
               15, TimeUnit.MINUTES)
           .build();

上面的代码的含义是在每小时的最后 15 分钟内运行定期工作。

3.1.4 工作约束

为了让工作在指定的环境下运行,我们可以给WorkRequest添加约束条件,常见的约束条件如下所示。

  • NetworkType:约束运行工作所需的网络类型,例如 Wi-Fi (UNMETERED)。
  • BatteryNotLow :如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。
  • RequiresCharging:如果设置为 true,那么工作只能在设备充电时运行。
  • DeviceIdle:如果设置为 true,则要求用户的设备必须处于空闲状态才能运行工作。
  • StorageNotLow:如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。

例如,以下代码会构建了一个工作请求,该工作请求仅在用户设备正在充电且连接到 Wi-Fi 网络时才会运行。

Constraints constraints = new Constraints.Builder()
       .setRequiredNetworkType(NetworkType.UNMETERED)
       .setRequiresCharging(true)
       .build();

WorkRequest myWorkRequest =
       new OneTimeWorkRequest.Builder(MyWork.class)
               .setConstraints(constraints)
               .build();

如果在工作运行时不满足某个约束,那么WorkManager 将停止工作,并且系统将在满足所有约束后重试工作。

3.1.5 延迟工作

如果工作没有约束,并且所有约束都得到了满足,那么当工作加入队列时系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动。

WorkRequest myWorkRequest =
      new OneTimeWorkRequest.Builder(MyWork.class)
               .setInitialDelay(10, TimeUnit.MINUTES)
               .build();

上面代码的作用是,设置任务在加入队列后至少经过 10 分钟后再运行。

3.1.6 重试和退避政策

如果需要让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();

3.1.7 标记WorkRequest

每个工作请求都有一个唯一标识符,该标识符可用于标识该工作,以便取消工作或观察其进度。如果有一组在逻辑上相关的工作,对这些工作项进行标记可能也会很有帮助。为WorkRequest添加标记使用的是addTag()方法,如下所示。

WorkRequest myWorkRequest =
       new OneTimeWorkRequest.Builder(MyWork.class)
       .addTag("cleanup")
       .build();

最后,可以向单个工作请求添加多个标记,这些标记在内部以一组字符串的形式进行存储。对于工作请求,我们可以通过 WorkRequest.getTags() 检索其标记集。

3.1.8 分配输入数据

有时候,任务需要输入数据才能正常运行。例如处理图片上传任务时需要上传图片的 URI 作为输入数据,我们将此种场景称为分配输入数据。

输入值以键值对的形式存储在 Data 对象中,并且可以在工作请求中设置,WorkManager 会在执行工作时将输入 Data 传递给工作,Worker 类可通过调用 Worker.getInputData() 访问输入参数,如下所示。

public class UploadWork extends Worker {

   public UploadWork(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);
       return Result.success();
   }
   ...
}

// Create a WorkRequest for your Worker and sending it input
WorkRequest myUploadWork =
      new OneTimeWorkRequest.Builder(UploadWork.class)
           .setInputData(
               new Data.Builder()
                   .putString("IMAGE_URI", "http://...")
                   .build()
           )
           .build();

上面的代码展示了如何创建需要输入数据的 Worker 实例,以及如何在工作请求中发送该实例。

3.2 Work状态

Work在其整个生命周期内经历了一系列 State 更改,状态的更改分为一次性任务的状态和周期性任务的状态。

3.2.1 一次性任务状态

对于一次性任务请求,工作的初始状态为 ENQUEUED。在 ENQUEUED 状态下,任务会在满足其 Constraints 和初始延迟计时要求后立即运行。接下来,该工作会转为 RUNNING 状态,然后可能会根据工作的结果转为 SUCCEEDEDFAILED 状态;或者,如果结果是 retry,它可能会回到 ENQUEUED 状态。在此过程中,随时都可以取消工作,取消后工作将进入 CANCELLED 状态。 在这里插入图片描述 上图展示了一次性工作的生命周期状态的变化过程,SUCCEEDED、FAILED 和 CANCELLED 均表示此工作的终止状态。如果您的工作处于上述任何状态,WorkInfo.State.isFinished() 都将返回 true。

3.2.2 定期任务状态

成功和失败状态仅适用于一次性任务和链式工作,定期工作只有一个终止状态 CANCELLED,这是因为定期工作永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。

在这里插入图片描述 上图展示了定时任务的生命周期状态的变化过程。

3.3 任务管理

3.3.1 唯一任务

在定义了Worker 和 WorkRequest之后,最后一步是将工作加入队列,将工作加入队列的最简单方法是调用 WorkManager enqueue() 方法,然后传递要运行的 WorkRequest。在将工作加入队列时需要注意避免重复加入的问题,为了实现此目标,我们可以将工作调度为唯一任务。

唯一任务可确保同一时刻只有一个具有特定名称的工作实例。与系统生成的ID不同,唯一名称是由开发者指定,而不是由 WorkManager 自动生成。唯一任务既可用于一次性任务,也可用于定期任务。您可以通过调用以下方法之一创建唯一任务序列,具体取决于您是调度重复任务还是一次性任务。

  • WorkManager.enqueueUniqueWork():用于一次性工作
  • WorkManager.enqueueUniquePeriodicWork():用于定期工作

并且,这两个方法都接受3个参数。

  • niqueWorkName:用于唯一标识工作请求的 String。
  • existingWorkPolicy:此 enum 可告知 WorkManager 如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作。如需了解详情,请参阅冲突解决政策。
  • work:要调度的 WorkRequest。

下面是使用唯一任务解决重复调度问题,代码如下。

PeriodicWorkRequest sendLogsWorkRequest = new
      PeriodicWorkRequest.Builder(SendLogsWorker.class, 24, TimeUnit.HOURS)
              .setConstraints(new Constraints.Builder()
              .setRequiresCharging(true)
          .build()
      )
     .build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
     "sendLogs",
     ExistingPeriodicWorkPolicy.KEEP,
     sendLogsWorkRequest);

上述代码在 sendLogs 作业时,如果已处于队列中的情况下运行则系统会保留现有的作业,并且不会添加新的作业。

3.3.2 冲突解决策略

有时候,任务的调度会出现冲突,此时我们需要告知 WorkManager 在发生冲突时要执行的操作,可以通过在将工作加入队列时传递一个枚举来实现此目的。对于一次性任务,系统提供了一个 ExistingWorkPolicy枚举累,它支持用于处理冲突的选项有如下几个。

  • REPLACE:用新工作替换现有工作。此选项将取消现有工作。
  • KEEP:保留现有工作,并忽略新工作。
  • APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。

现有工作将成为新工作的先决条件,如果现有工作变为 CANCELLEDFAILED 状态,新工作也会变为 CANCELLEDFAILED。如果您希望无论现有工作的状态如何都运行新工作,那么可以使用 APPEND_OR_REPLACEAPPEND_OR_REPLACE的作用是不管状态变为 CANCELLEDFAILED 状态,新工作仍会运行。

3.4 观察任务状态

在将任务加入到队列后,我们可以根据 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) 设置的任何输出数据。利用每个方法的 LiveData ,我们可以通过注册监听器来观察 WorkInfo 的变化,如下所示。

workManager.getWorkInfoByIdLiveData(syncWorker.id)
        .observe(getViewLifecycleOwner(), workInfo -> {
    if (workInfo.getState() != null &&
            workInfo.getState() == WorkInfo.State.SUCCEEDED) {
        Snackbar.make(requireView(),
                    R.string.work_completed, Snackbar.LENGTH_SHORT)
                .show();
   }
});

并且,WorkManager 2.4.0 及更高版本还支持使用 WorkQuery 对象对已加入队列的作业进行复杂查询,WorkQuery 支持按工作的标记、状态和唯一工作名称的组合进行查询,如下所示。

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);

上面代码的作用是查找带有“syncTag”标记、处于 FAILED 或 CANCELLED 状态,且唯一工作名称为“preProcess”或“sync”的所有任务。

3.5 取消和停止任务

3.5.1 取消任务

WorkManager支持取消对列中的任务,取消时按工作的 name、id 或与其关联的 tag来进行取消,如下所示。

// by id
workManager.cancelWorkById(syncWorker.id);

// by name
workManager.cancelUniqueWork("sync");

// by tag
workManager.cancelAllWorkByTag("syncTag");

WorkManager 会在后台检查任务的当前State。如果工作已经完成,系统不会执行任何操作。否则工作的状态会更改为 CANCELLED,之后就不会运行这个工作。

3.5.2 停止任务

正在运行的任务可能因为某些原因而停止运行,主要的原因有以下一些。

  • 明确要求取消它,可以调用WorkManager.cancelWorkById(UUID)方法。
  • 如果是唯一任务,将 ExistingWorkPolicy 为 REPLACE 的新 WorkRequest 加入到了队列中时,旧的 WorkRequest 会立即被视为已取消。
  • 添加的任务约束条件不再适合。
  • 系统出于某种原因指示应用停止工作。

当任务停止后,WorkManager 会立即调用 ListenableWorker.onStopped()关闭可能保留的所有资源。

3.6 观察任务的进度

WorkManager 2.3.0为设置和观察任务的中间进度提供了支持,如果应用在前台运行时,工作器保持运行状态,那么也可以使用WorkInfo 的 LiveData Api向用户显示此信息。ListenableWorker 支持使用setProgressAsync() 方法来保留中间进度。ListenableWorker只有在运行时才能观察到和更新进度信息。

3.6.1 更新进度

对于Java 开发者来说,我们可以使用 ListenableWorker 或 Worker 的 setProgressAsync() 方法来更新异步过程的进度。耳低于 Kotlin 开发者来说,则可以使用 CoroutineWorker 对象的 setProgress() 扩展函数来更新进度信息。 ,如下所示。 Java写法:

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();
    }
}

Kotlin写法:

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import kotlinx.coroutines.delay

class ProgressWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    companion object {
        const val Progress = "Progress"
        private const val delayDuration = 1L
    }

    override suspend fun doWork(): Result {
        val firstUpdate = workDataOf(Progress to 0)
        val lastUpdate = workDataOf(Progress to 100)
        setProgress(firstUpdate)
        delay(delayDuration)
        setProgress(lastUpdate)
        return Result.success()
    }
}

3.6.2 观察进度

观察进度可以使用 getWorkInfoBy…() 或 getWorkInfoBy…LiveData() 方法,此方法会返回 WorkInfo信息,如下所示。

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
             }
      }
});

参考:

Android Jetpack架构组件(六)之Room
Android Jetpack架构组件(五)之Navigation
Android Jetpack架构组件(四)之LiveData
Android Jetpack架构组件(三)之ViewModel
Android Jetpack架构组件(二)之Lifecycle
Android Jetpack架构组件(一)与AndroidX