前语

经过上一篇 Android WorkManager 初探 的介绍,基本了解了 WorkManager 的特色,经过履行单次使命的示例,现已感受到了其强壮之处。下面,了解一下如何运用 WorkManager 履行周期性使命。

周期性使命

在上一篇中咱们了解到,WorkManager 能够依据设备当前的特性(包括网络、电量状态、存储空间等要素)束缚使命履行的条件,一起还能够依据使命履行的结果进行设置不同的重试战略,下面咱们就经过一个日志上传的使命来了解一下 WorkManager 更多的东西。

  • 方针:日志上传,每一小时上传一次。只要在设备处于充电且 Wifi 连接时才能够履行。电量低时不允许履行,失利后需求依据指定的战略重试。
  1. 界说 Worker
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。
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

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. 增加使命到队列
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


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 会在各个使命之间主动传递,直到有过错产生或许履行成功。

    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 的初始化。

 <!-- 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 完成。更多的功用能够参阅官方供给的指导文档,结合实践事务场景进行运用,会有更具体的领会和认识。

参阅文档