Android WorkManager 周期性任务的使用

前言

通过上一篇 Android WorkManager 初探 的介绍,基本了解了 WorkManager 的特点,通过执行单次任务的示例,已经感受到了其强大之处。下面,了解一下如何使用 WorkManager 执行周期性任务。

周期性任务

在上一篇中我们了解到,WorkManager 可以基于设备当前的特性(包括网络、电量状态、存储空间等因素)约束任务执行的条件,同时还可以基于任务执行的结果进行设置不同的重试策略,下面我们就通过一个日志上传的任务来了解一下 WorkManager 更多的东西。

  • 目标:日志上传,每一小时上传一次。只有在设备处于充电且 Wifi 连接时才可以执行。电量低时不允许执行,失败后需要根据指定的策略重试。
  1. 定义 Worker
kotlin 复制代码
class UploadUserLogWork(appContext: Context, workerParameters: WorkerParameters) :
    CoroutineWorker(appContext, workerParameters) {
    override suspend fun doWork(): Result {
        val userId = inputData.getString(INPUT_TAG)

        if (TextUtils.isEmpty(userId)) {
            return Result.failure(Data.Builder().putString(OUTPUT_TAG, "userId is null").build())
        }

        val result = uploadLog(userId)
        return if (result != null) {
            val output = Data.Builder().putString(OUTPUT_TAG, result).build()
            Result.success(output)
        } else {
            Result.retry()
        }
    }

    override suspend fun getForegroundInfo(): ForegroundInfo {
        return ForegroundInfo(
            1, createNotification(
                applicationContext, id, applicationContext.getString(R.string.app_name)
            )
        )
    }
}
  • 这里结合 Kotlin Coroutine 使用,因此定义 Worker 时需要继承 CoroutineWorker。除了实现 doWork 方法之外,还要实现 getForegroundInfo 方法,返回一个执行职务时的 Notification 即可。当然,如果你对 RxJava 更熟悉,也有相应的基类可以使用。
  • 上传日志需要结合 userId 参数,参数为空时直接按失败处理。
  • uploadLog 方法如果执行成功,按执行成功处理,否则返回 retry。
kotlin 复制代码
private suspend fun uploadLog(userId: String?): String? {

    return withContext(Dispatchers.IO) {
        Log.i(TAG, "start upload")
        delay(20000)
        if (System.currentTimeMillis().toInt() % 1000 == 0) {
            Log.i(TAG, "upload fail")
            null
        } else {
            Log.i(TAG, "finish upload")
            "${userId}_${System.currentTimeMillis()}"
        }
    }
}

这里简单模拟一个日志上传的任务,同时基于时间戳 mock 上传失败的场景,方便测试。

  1. 创建 WorkRequest
kotlin 复制代码
fun createWorkRequest(userId: String?): PeriodicWorkRequest {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)
        .setRequiresCharging(true)
        .setRequiresBatteryNotLow(true)
        .build()


    return PeriodicWorkRequestBuilder<UploadUserLogWork>(1, TimeUnit.HOURS)
        .setInputData(workDataOf(INPUT_TAG to userId))
        .setConstraints(constraints)
        .setBackoffCriteria(BackoffPolicy.LINEAR, 3, TimeUnit.SECONDS)
        .addTag(WORK_TAG)
        .build()
}
  • 首先定义约束条件,NetworkType.UNMETERED 的意思是不按用量计费的网络,我们可以简单理解为 WiFi,非手机流量。其他的约束条件顾名思义。Constraints 的构造者模式还提供了其他的约束,比如设备是否处于空闲,存储容量是否过低等,我们基于实际情况做选择就好。当然,这些约束条件有些是互斥的,或者和重试策略是有冲突的,设置不恰当的话会在执行时抛出异常,毕竟万事不能既要又要还要。
  • 通过 PeriodicWorkRequestBuilder 创建周期性的任务,这里是每一小时执行一次,需要注意的是最小间隔不能小于 15 分钟。即便你设置过小的时间,内部也会约束到 15 分钟。
  • 将 userId 通过 InputData 传入,这个上一篇已经用过了。
  • setBackoffCriteria 是用来设置重试策略的,当 doWork 方法返回 Result.retry 的时候,就会基于这里设置的策略进行重试。这里的机制是失败后首次延迟 3 秒重试,并且后续时间按线性增长,也就是 6,9,12 秒的时候执行。默认的增长策略是2的指数级增长。也就是会在 6,12,24 秒执行,这个基于实际场景做调整即可。这里需要注意的是,首次延迟重试的时间不得大于 10 秒,同时这个延迟的最大时间是 5 小时,不会无限制的增长。
  • 最后给 WorkRequest 添加 TAG,方便后续查询这个 WokerRequest 的信息及对其进行管理。
  1. 添加任务到队列
kotlin 复制代码
fun triggerWork(application: Application) {
    // userId = "123"
    val request = createWorkRequest("123")
    WorkManager.getInstance(application).enqueueUniquePeriodicWork(
        WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, request
    )
}
  • enqueueUniquePeriodicWork 方法接受 3 个参数:
    • uniqueWorkName - 用于唯一标识工作请求的 String。
    • existingWorkPolicy - 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作,这个枚举有 4 个值。
      • REPLACE:用新工作替换现有工作。此选项将取消现有工作。
      • KEEP:保留现有工作,并忽略新工作。
      • APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。现有工作将成为新工作的先决条件。如果现有工作变为 CANCELLED 或 FAILED 状态,新工作也会变为 CANCELLED 或 FAILED。如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE。
      • APPEND_OR_REPLACE 函数类似于 APPEND,不过它并不依赖于先决条件工作状态。即使现有工作变为 CANCELLED 或 FAILED 状态,新工作仍会运行。
    • work - 要调度的 WorkRequest。

对于周期性的任务只支持 REPLACE 和 KEEP 这两种策略。 对于日志上传的场景,我们只需要定义一个任务之后,希望他后续按周期执行即可。因此这里的策略设置为 KEEP 即可。

  1. 启动任务

由于是周期性执行的任务,我们找到一个合适的位置调用一次 triggerWork 方法即可一劳永逸了,看一下日志。

这里为了方便测试,将 uploadLog 方法中发生错误的条件改为了 System.currentTimeMillis().toInt() % 2 == 0

kotlin 复制代码
21:45:36.633 25520-25579 WorkManagerPlayground   com.engineer.android.mini            I  start upload
21:45:56.640 25520-25579 WorkManagerPlayground   com.engineer.android.mini            I  upload fail
21:46:06.690 25520-25579 WorkManagerPlayground   com.engineer.android.mini            I  start upload
21:46:26.694 25520-25579 WorkManagerPlayground   com.engineer.android.mini            I  upload fail
21:46:46.775 25520-25579 WorkManagerPlayground   com.engineer.android.mini            I  start upload
21:47:06.780 25520-25579 WorkManagerPlayground   com.engineer.android.mini            I  finish upload

可以看到失败之后,第一次延时 10 秒后进行了重试,第二此延时是 20 秒,并且成功了。

从这个例子可以看到,WorkManager 的设计非常实用,尤其是其基于约束条件的任务调度,重试策略等。这些在实际业务开发中经常会遇到,比如网络请求失败后怎么重试?无限轮询,服务器能抗住吗?如果是由于 Bug 导致的意外失败,短时间内大规模的重试会导致雪崩,发生恶性循环将整个服务打挂。而 WorkManger 内置了线性增长或者是指数级增长的 API 就显得非常友好。这里无论是否使用 WorkManager ,看一下源码学习一下这些约束条件的实现也是很有收获的,看看 Android 团队的人是怎么写代码的,是否有值得借鉴的地方。

编排任务

除了安排周期性任务,WorkManger 另一个特点就是编排任务 的功能。这里以官方示例 WorkManagerSample ,中的代码做示范。

下面的功能是对一张图片做滤镜,首先会清理遗留文件,然后会依次执行 WaterColor/GrayScale/BlurEffec 这几个滤镜效果,而是否执行时根据传入的参数在扩展函数中做判断。最后,根据 save 字段执行 SaveImageToGalleryWorker 将图片保存在本地相册或者是上传到服务器。这个过程中,数据 InputData 会在各个任务之间自动传递,直到有错误发生或者执行成功。

kotlin 复制代码
    init {
        continuation = WorkManager.getInstance(context).beginUniqueWork(
            Constants.IMAGE_MANIPULATION_WORK_NAME,
            ExistingWorkPolicy.REPLACE,
            OneTimeWorkRequest.from(CleanupWorker::class.java)
        ).thenMaybe<WaterColorFilterWorker>(waterColor).thenMaybe<GrayScaleFilterWorker>(grayScale)
            .thenMaybe<BlurEffectFilterWorker>(blur).then(
                if (save) {
                    workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
                } else {
                    workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
                }
            )
    }

    /**
     * Applies a [ListenableWorker] to a [WorkContinuation] in case [apply] is `true`.
     */
    private inline fun <reified T : ListenableWorker> WorkContinuation.thenMaybe(
        apply: Boolean
    ): WorkContinuation {
        return if (apply) {
            then(workRequest<T>())
        } else {
            this
        }
    }

这里通过定义 thenMaybe 扩展函数实现了基于参数控制任务执行与否的封装。这里其实特别像 RxJava 或者是 Java StreamAPI, 通过流式 API 将一些复杂的操作串起来执行,尤其是当这些任务有依赖关系的时候,WorkManager 提供的这类编排机制就更加友好了。日常开发中,Application 中经常会有大量的初始化,而这些初始经常会有依赖关系,比如很多三方库依赖网络库的初始化,有些则依赖系统权限的获取,总之时间久了就变成一锅粥了。如果能合理的使用 WorkMangaer 对这些任务进行编排,势必会减少后期的维护成本。

关于初始化

这两篇关于 WorkManager 的文章,始终没有提及 WorkManager 的初始化。其实他和大名鼎鼎的 LeakCanary 一样,是通过 ContentProvider 进行初始化,如果你的应用对启动时间(无论是冷启动还是热启动)都非常敏感的话,可以通过以下方式移除 WorkManager 的初始化。

xml 复制代码
 <!-- If you want to disable android.startup completely. -->
 <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove">
 </provider>

但同时,你需要自己在合适的位置手动进行初始化。

小结

WorkManger 的功能非常强大,由于其任务是通过 SQLite 数据库持久化存储在本地,因此相比以往的线程机制,对任务的可控性强了很多,尤其是周期性的任务。无论是任务进度的查询,还是任务的取消都可以结合其 API 实现。更多的功能可以参考官方提供的指导文档,结合实际业务场景进行使用,会有更具体的体会和认识。

参考文档

相关推荐
Acacia.~3 分钟前
第八章 利用CSS制作导航栏
前端·css
编程老船长27 分钟前
网页设计基础 第十九讲:CSS定位实战 —— 打造精美布局的个人简介页
前端·css·html
张铁铁是个小胖子34 分钟前
整合seata遇到的问题
android
snow_wind_rain1 小时前
网页作业9
前端·css·css3
zhzhzhen_1 小时前
如何在项目中用elementui实现分页器功能
前端·javascript·elementui
向明天乄1 小时前
elementui el-table中给表头 el-table-column 加一个鼠标移入提示说明
前端·javascript·vue.js·elementui
Akiiiira1 小时前
【网页设计】CSS3 进阶(动画篇)
前端·javascript·css3
蒜蓉大猩猩2 小时前
Vue3.js - 一文看懂Vuex
前端·javascript·vue.js·前端框架·html5
excel2 小时前
three EdgeSplitModifier
前端
一航jason2 小时前
Android Jetpack Compose 现有Java老项目集成使用compose开发
android·java·android jetpack