Jetpack —— WorkManager 处理后台任务

Jetpack 是谷歌发布的一套工具,可以帮助开发者更轻松的编写优质应用。

其中架构组件包括了:

  • Lifecycles,可以管理 Activity 和 Fragment 的生命周期
  • LiveData,在底层数据库更改的时候通知视图
  • Navigation,处理应用内导航所需的一切
  • Paging,逐步从数据源中加载信息
  • Room,流畅的访问 SQLite 数据库
  • ViewModel,以注重生命周期的方式管理界面相关的数据
  • WorkManager,管理 Android 后台作业

我看了一圈官网,似乎讲的也不是很明确,搜了一圈,大部分博客都是将官网内容抄了一遍完事儿,我试着按照我自己的理解些一下。

先从 WorkManager 开始

什么是 WorkManager

在实际开发中,经常会有这样的需求,当用户连上 WIFI 并且充电时,向服务器发送日志、定时的长传图片、定时同步数据等等,这些工作都需要在后台操作,通常我们是搭配 Service + Broadcast 来完成工作的,大量的后台任务导致电量的不正常消耗使得谷歌对后台行为做了大量限制并且对我们都后台操作进行了建议:

可以看到当我们的任务不需要立即完成的话,就可以选择 WorkManager,它可以在满足条件(例如是否充电、是否联网)时运行后台工作。

值得一说的是,WorkManager 可以自动维护后台任务,同时可适应不同的条件,同时满足后台 Service 和静态广播,内部维护着 JobScheduler,而在 6.0 以下系统版本则可自动切换为 AlarmManager,简直是一套武器四处耍,后台任务的不二之选。

WorkManager 不适用于需要立即执行的任务,也不适用于应用进程被杀死之后的任务执行,也就是说,有些博客上写的 WorkManager 可以实现保活动能是假的,WorkManager 只是将任务保存在一个数据库中,当任务条件达到并且打开应用之后,这个任务才会执行。

WorkManager 使用

引入

  1. 添加运行库
allprojects {
    repositories {
        google()
        jcenter()
    }
}
  1. 添加依赖项
 dependencies {
    def work_version = "2.1.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 - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"
    }

WorkManager 支持 compileSdk 版本 28 或更高版本。

几个关键类

WorkManager API 建立在这几个类上,我们必须继承一些抽象类来完成任务的安排

  • Worker:等同于需要在后台执行的任务。我们需要实现它的子类,在其中完成后台任务。
  • WorkRequest:工作调度,为每个后台任务创建请求、为工作累创建约束条件,有两个直接子类:

    • OneTimeWorkRequest,用于非重复任务,默认情况下是立即执行,但是会受到约束条件的限制
    • PeriodicWorkRequest,用于重复任务,同样的也会受到约束条件的限制
  • WorkManager:基于 WorkRequest 中定义的约束来管理和调度任务的类
  • WorkStatus:包装了 Work 请求状态的类。

创建后台任务

创建 Worker 的子类,重写 doWork() 方法,我们的后台任务逻辑就是在这个方法中运行,该方法执行在子线程当中,该方法始终返回三个结果:

  • 成功(Result.success())
  • 失败(Resule.failure())
  • 需要重试(Result.retry())

例如定期上传日志文件的任务:

public class UploadWorker extends Worker {
    private static final String TAG = "TTT";

    private Context context;

    public UploadWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
        this.context = context;
    }

    @Override
    public Result doWork() {
        File file = new File(context.getFilesDir() + "/log.log");
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("https://xxxxxxxxxx")
                .post(RequestBody.create(file, MediaType.parse("text/x-markdown; charset=utf-8")))
                .build();
        try {
            Response response = client.newCall(request).execute();
            return Result.success();
        } catch (IOException e) {
            e.printStackTrace();
            return Result.failure();
        }
    }
}

需要注意的是,系统规定 Worker 执行任务的时间为最长 10 分钟,超时任务将会被停止。

每个 Worker 实例只调用一次 doWork()。如果需要重新运行工作单元,则会创建新的工作程序。

配置任务的运行时间和运行方法

使用 WorkRequest 来配置任务的运行时间和运行方式,对于单次任务,可以使用 OneTimeWorkRequestPeriodicWorkRequest

一个简单的例子:

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

WorkRequest 还可以包含任务的运行条件、工作输入、延迟以及重试机制等等功能,在后面会讲。

执行任务

在 WorkRequest 创建完成之后,就可以调用 enqueue() 方法使用 WorkManager 对其进行计划。

WorkManager.getInstance().enqueue(uploadWorkRequest);

默认情况下,系统会立即执行我们的任务,但是当没有联网的时候,则会执行失败,我们可以通过添加限定条件来让我们的任务在特定情况下才会执行。

添加约束条件

通过调用 WorkRequest 的 setConstraints 方法来设置任务的执行环境,WorkManager 提供了以下的限定:

  • setRequiresBatteryNotLow

执行任务时电池电量不能偏低。

  • setRequiresCharging

在设备充电时才能执行任务。

  • setRequiresDeviceIdle

设备空闲时才能执行。

  • setRequiresStorageNotLow

设备储存空间足够时才能执行。

  • addContentUriTrigger

指定是否在(Uri 指定的)内容更新时执行本次任务(只能用于 Api24 及以上版本)

  • setRequiredNetworkType

指定任务执行时的网络状态。其中状态如下:

  • NOT_REQUIRED:不需要网络
  • CONNECTED:任何可用网络
  • UNMETERED:需要不计量网络,如WiFi
  • NOT_ROAMING:需要非漫游网络
  • METERED:需要计量网络,如4G

可以同时为 WorkRequest 设置多个约束条件:

Constraints constraints = new Constraints.Builder().setRequiresCharging(true).setRequiredNetworkType(NetworkType.UNMETERED).build();
OneTimeWorkRequest build = new OneTimeWorkRequest.Builder(UploadWorker.class).setConstraints(constraints).build();

如果设置多个约束条件,当所有条件全部满足的时候才会执行,上述约束约定当充电并且连接 WIFI 的时候才会执行我们的后台任务。

延时执行

我们也可以选择我们的任务不立即执行而是延后执行,可以调用 setInitialDelay 方法将任务延时:

OneTimeWorkRequest build = new OneTimeWorkRequest.Builder(UploadWorker.class).setConstraints(constraints).setInitialDelay(10, TimeUnit.MINUTES).build();

上述代码保证任务在满足约束条件至少十分钟之后执行。之所以说至少,是因为任务还会受到 WorkRequest 和系统优化中使用的约束。

任务重试

当我们的任务出错,可以在 Worker 中返回 Result.retry() 表示该任务需要重试,在 WorkRequest 中调用 setBackoffCriteria 方法定义重试策略,该方法需要两个参数:

  • backoffPolicy,官方网站翻译为 退避策略,其实我感觉实际翻译成重试规则会比较贴切,它有两选项:

    • BackoffPolicy.EXPONENTIAL:指数
    • BackoffPolicy.LINEAR:线性

二者的区别我也不是很懂,官方上也没有很清晰的解释,按照字面意思大概就是每次重试的间隔一个是匀称的,一个是成倍数的吧。

  • duration,重试之前的等待时间

重试之前的等待时间也可以通过另外两个参数来确定:backoffDelay 和 timeUnit,一个是时长,一个是单位。

                OneTimeWorkRequest build = new OneTimeWorkRequest.Builder(UploadWorker.class)
                        .setConstraints(constraints)
                        .setInitialDelay(10, TimeUnit.MINUTES)
                        .setBackoffCriteria(
                                BackoffPolicy.LINEAR,
                                OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                                TimeUnit.MILLISECONDS)
                        .build();

当 Worker 返回 Result.retry() 的时候,会按照上面的重试策略来进行重试

输入和输出

有时候我们的任务需要一些参数,或者说我们需要任务的一些结果,WorkManager 提供了 Data 类作为数据的载体,数据使用键值对的方式保存在 Data 对象中,调用 WorkRequest 的 setInputData 方法将 Data 对象传入,在 Worker 中调用 getInputData 方法获取传入的数据:

build = new OneTimeWorkRequest.Builder(UploadWorker.class)
                .setInputData(new Data.Builder().putString("Key", "Value").build())
                .build();

在 Worker 中调用 getInputData() 方法获取到 Data 对象,就可以获取到传入的数据了。

输出也很简单,在 doWork 方法返回的 success 或者 failure 方法中将 Data 作为参数传入即可,接受输出也很简单:

instance.getWorkInfoByIdLiveData(build.getId()).observeForever(new Observer<WorkInfo>() {
    @Override
    public void onChanged(WorkInfo workInfo) {
        if(workInfo.getState() == WorkInfo.State.SUCCEEDED){
            Log.d(TAG, "Succeeded onChanged: " + workInfo.getOutputData().getString("Result"));
        }
    }
});

Data 对象的值可以是基本类型和字符串,并且最大限制为 10K。

为任务分组

为方便管理任务,可以为不同的任务添加 tag,方法如下:

build = new OneTimeWorkRequest.Builder(UploadWorker.class)
    .addTag("upload")
    .setInputData(new Data.Builder().putString("Key", "Value").build())
    .build();

为任务添加分组之后,可以就可以方便的批量操控任务了:

取消任务

instance.cancelAllWorkByTag("upload");

获取同批任务

instance.getWorkInfosByTagLiveData("upload")

任务的状态

还记得在输入/输出环节中的一行判断语句吗?

if(workInfo.getState() == WorkInfo.State.SUCCEEDED){
    ...
}

上面的代码表示当我们的任务已经是 SUCCEEDED 状态时,才执行代码中的操作,WorkManager 将 Worker 分为以下几个状态:

  • BLOCKED:对于约束条件还没有满足的任务,处于 BLOCKED 状态,也叫阻塞状态。
  • ENQUEUED:对于已经满足约束条件和执行时间的任务,但还没有执行的,处于 ENQUEUED 状态,也叫入列状态。
  • RUNNING:正在执行的任务处于 RUNNING 状态。
  • SUCCEEDED:执行成功的任务处于 SUCCEEDED 状态。
  • FAILED:执行失败的任务处于 FAILED 状态。
  • CANCELLED:已经被用户标记为取消(调用了 cancelXXX 方法)但尚未终止的任务,处于 CANCELLED 状态。

在任务进入队列之后,WorkManager 提供了 WorkInfo 对象来帮助我们观察任务,WorkInfo 对象包括了任务的 ID、标记、当前状态以及输出数据。

可以通过以下三种方法获取到 WorkInfo:

  • WorkManager.getWorkInfoById(UUID)或 WorkManager.getWorkInfoByIdLiveData(UUID)

UUDI 可以通过任务对象 getId() 方法获得。

  • WorkManager.getWorkInfosByTag(String)或WorkManager.getWorkInfosByTagLiveData(String)

这两个方法适用于设置了 tag 的任务。

  • WorkManager.getWorkInfosForUniqueWork(String)或 WorkManager.getWorkInfosForUniqueWorkLiveData(String)

这两个方法适用于工作链,后面会讲到。

例子就不写了,输入/输出那段已经有了。

任务链

WorkManager 还可以将多个任务组成任务链来执行,其中一个任务的输出值可以作为另一个任务的输入值。但需要注意的是,这种任务链只支持 OneTimeWorkRequest

调用 WorkManager 的 beginWith 方法添加开始任务,参数可以是一个任务(OneTimeWorkRequest)也可以是一个存储任务的 List:

WorkManager.getInstance()
    // filter1、filter2、filter3 三个任务并行执行
    .beginWith(Arrays.asList(filter1, filter2, filter3))
    // beginWith 中所有任务执行完成之后执行
    .then(compress)
    .then(upload)
    // 不要忘了 enqueue()
    .enqueue();

上一个任务的输出值可以作为下一个任务的输入值,对于上面例子中多个输出内容,WorkManager 提供了下面两种模式:

  • OverwritingInputMerger 尝试将所有输入的所有键添加到输出。 如果发生冲突,它会覆盖先前设置的 Key。
  • ArrayCreatingInputMerger 尝试合并输入,并在必要时创建数组。

对于上面的示例,假设我们要保留所有 filter 的输出,我们应该使用 ArrayCreatingInputMerger。

在一个任务链中,当父任务执行成功(返回 Result.success()),该任务仅仅进入BLOCKED(阻塞状态),而不是停止;当父任务执行出错(返回 Result.failure()),所有依赖它的任务都会进入 FAILED 状态;取消任意父任务,所有依赖它的子任务都会进入 CANCELLED 状态。

任务的取消和停止

如果我们想要取消已经添加的任务,可以通过调用 WorkManager 提供的一系列 cancelXXX 方法:

  • cancelWorkById
  • cancelAllWorkByTag
  • cancelAllWork
  • cancelUniqueWork

当调用了取消方法之后,WorkManager 会检查任务的状态,如果任务已经完成(State 处于 SUCCEEDED 状态),不会做什么操作(这不废话么,嫁出去的姑娘,泼出去的水,还有完成了的任务),如果任务还没有完成,则将任务标记为取消状态(CANCELLED),标记为取消状态的任务不会再执行,其任务链上的剩余子任务也不会执行。

当出现以下几种情况时,将会调用任务的 onStopped 方法:

  1. 用户调用 cancelXXX 方法取消任务
  2. Unique 任务中,用户使用新的任务替换了旧任务
  3. 任务限制不符合
  4. 系统原因停止任务

一般我们在 onStopped 方法中进行一些收尾操作,例如关闭数据库,关闭数据流等等。可以通过调用 isStopped 方法来判断任务是否停止。

即使在调用了 onStopped 方法之后 Worker 返回了执行结果,系统也会无视之。

循环任务

一些定时任务例如上传日志、数据备份等需要定时执行,可以使用 PeriodicWorkRequest

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

PeriodicWorkRequest saveRequest =
        new PeriodicWorkRequest.Builder(SaveImageFileWorker.class, 1, TimeUnit.HOURS)
                  .setConstraints(constraints)
                  .build();

WorkManager.getInstance()
    .enqueue(saveRequest);

在上面的例子中,每一个小时就会运行一次保存图片的任务,但这个时间间隔并不是十分精确的,它还受到约束条件以及设备自身的一些元素的制约。

  1. WorkManager 规定循环任务的最小间隔时间为 15 分钟
  2. 循环任务无法使用任务链

Unique 任务

这个不知道该咋翻译,翻译成独特任务好像也不贴切,它的功能是可以对任务链中的任务进行一些其他操作,例如替换、附加等操作。

例如你已经添加了一个任务,名为 A,当你再添加一个名称 A 的任务的时候,你有以下选择:

  • REPLACE 替换原来的 A
  • KEEP 保持原来的 A,忽略你新添加的 A
  • APPEND 原来的 A 执行完成后,执行新添加的 A

APPEND 不能和重复任务(PeriodicWorkRequest)搭配使用

创建 Unique 任务也很简单:

  • WorkManager.enqueueUniqueWork(String,ExistingWorkPolicy,OneTimeWorkRequest)
  • WorkManager.enqueueUniquePeriodicWork(String,ExistingPeriodicWorkPolicy,PeriodicWorkRequest)

第一个参数是 Unique 的名称,之前章节中的 cancelUniqueWork 方法参数就是这个。

第二个参数是上面讲的三种规则:

  1. ExistingWorkPolicy.REPLACE
  2. ExistingWorkPolicy.KEEP
  3. ExistingWorkPolicy.APPEND

第三个参数是任务或者任务 List。