Android WrokManager

903 阅读4分钟

WorkManager 简介

WorkManager 是一个 API,可供轻松调度那些即使在退出应用或重启设备后仍应运行的可靠异步任务。WorkManager API 是一个适合用来替换所有先前的 Android 后台调度 API(包括 FirebaseJobDispatcherGcmNetworkManager 和 JobScheduler)的组件,我们也建议你这样做。WorkManager 在其现代、一致的 API 中整合了其前身的功能,该 API 支持 API 级别 14,在开发时即考虑到了对电池续航的影响。

为什么要使用 WorkManager

WorkManager 运行后台工作,同时处理兼容性问题和电池和系统健康的最佳实践。

此外,使用 WorkManager,可以调度周期性任务和复杂的依赖任务链:后台工作可以并行或顺序执行,你可以在其中指定执行顺序。WorkManager 无缝地处理在任务之间传递输入和输出。

你还可以设置后台任务何时运行的标准。例如,如果设备没有网络连接,就没有理由向远程服务器发出 HTTP 请求。因此,你可以设置一个约束,该任务只能在存在网络连接时运行。

作为保证执行的一部分,WorkManager 负责在设备或应用程序重新启动时保持你的工作。如果你的工作停止并且你想稍后重试,你还可以轻松定义重试策略。

最后,WorkManager 允许你观察工作请求的状态,以便你可以更新 UI。

总而言之,WorkManager 具有以下优势:

  • 处理与不同操作系统版本的兼容性
  • 遵循系统健康最佳实践
  • 支持异步一次性和周期性任务
  • 支持具有输入/输出的链式任务
  • 允许你在任务运行时设置约束
  • 保证任务执行,即使应用程序或设备重新启动

何时使用 WorkManager

后台处理指南中相关博客

后台处理指南

WorkerManager的工作流程

在后台,WorkManager 根据以下条件使用底层作业来调度服务: image.png

如何使用 WorkManager

引入对于的 lib

// (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"

WorkRequest

WorkRequest 是一个抽象类,它有两种实现

  • OneTimeWorkRequest 一次性任务
  • PeriodicWorkRequest 周期性任务

创建简单 OneTimeWorkRequest

WorkManagerActivity 类

class WorkManagerActivity : BaseActivity() {
    override fun initLayout() = R.layout.activity_workmanager

   fun onClickStartTask(v: View) {
       val request = OneTimeWorkRequest.Builder(BlurWorker::class.java).build()
       // 第一种写法
       WorkManager.getInstance(this).enqueue(request)
   }
}

BlurWorker 类

private const val TAG = "BlurWorker"

private const val DEFAULT_WORKER_ID = 10086

/**
 * worker id
 */
const val TEST_WORKER_KEY_ID = "test_worker_key_id"

class BlurWorker(appContext: Context, workerParams: WorkerParameters) :
    CoroutineWorker(appContext, workerParams) {
    /**
     * doWork()方法有三种类型的返回值:
     * 执行成功返回Result.success()
     * 执行失败返回Result.failure()
     * 需要重新执行返回Result.retry()
     */
    override suspend fun doWork(): Result {
        Log.d(TAG, "开始执行")
        fakeBlur()
        Log.d(TAG, "执行结束")
        return Result.success()
    }

    private suspend fun fakeBlur() {
        delay(1000)
    }

以上就是一个简单的单次任务

  • 创建 OneTimeWorkRequest 的任务
  • 获取一个 WorkManager 的示例并且将任务加入到队列中

向 WorkerRequest 发送数据

  • 有的时候需要向任务发送一些数据,比方说需要压缩的图片路径、上传的图片路径等等,都可以通过WorkRequestsetInputData(data) 传入文件路径,例:
// 传入 id
val data = Data.Builder().put(TEST_WORKER_KEY_ID, 10010).build()
val request = OneTimeWorkRequest.Builder(BlurWorker::class.java)
    .setInputData(data)
    .build()
WorkManager.getInstance(this).enqueue(request)

在对应的 Worker 中进行获取发送的数据

class BlurWorker(appContext: Context, workerParams: WorkerParameters) :
    CoroutineWorker(appContext, workerParams) {
    override suspend fun doWork(): Result {
        fakeBlur(inputData.getInt(TEST_WORKER_KEY_ID, DEFAULT_WORKER_ID))
        return Result.success()
    }

    private suspend fun fakeBlur(workId: Int) {
        Log.d(TAG, "开始执行,任务 ID: $workId")
        delay(1000)
        Log.d(TAG, "fakeBlur: 成功")
    }

}

WorkerRequest 除了设置输入数据还能设置很多限制条件,比如设置初始延迟时间

  • 设置延迟执行任务。假设你没有设置触发条件,或者当你设置的触发条件符合系统的执行要求,此时,系统有可能立刻执行该任务,但如果你希望能够延迟执行,那么可以通过 setInitialDelay() 方法,延后任务的执行。
/**
 * 设置WorkRequest的初始延迟。
 * 参数:
 * 持续时间 - 以timeUnit单位的延迟长度
 * timeUnit – duration时间的duration单位
 * 返回:
 * 当前的WorkRequest.Builder
 * 抛出:
 * IllegalArgumentException – 如果给定的初始延迟将使执行时间超过Long.MAX_VALUE并导致溢出
 */
public @NonNull B setInitialDelay(long duration, @NonNull TimeUnit timeUnit) {
    mWorkSpec.initialDelay = timeUnit.toMillis(duration);
    if (Long.MAX_VALUE - System.currentTimeMillis() <= mWorkSpec.initialDelay) {
        throw new IllegalArgumentException("The given initial delay is too large and will"
                + " cause an overflow!");
    }
    return getThis();
}
  • 设置指数退避策略。假如 Worker 的执行出现了异常,比如网络较弱文件上传失败,你希望过段时间重试该任务。那么你可以在 WorkerdoWork() 方法中返回 Result.retry(),系统会有默认的指数退避策略来帮你重试任务,你也通过设置 setBackoffCriteria() 来设置退避策略和退避延迟
/**
 * 设置工作的退避策略和退避延迟。 默认值分别是BackoffPolicy.EXPONENTIAL和30000 。 duration将被限制在MIN_BACKOFF_MILLIS和MAX_BACKOFF_MILLIS之间。
 * 参数:
 * backoffPolicy – 增加退避时间时使用的BackoffPolicy
 * 持续时间 - 重试工作前的等待时间
 * 返回:
 * 当前的WorkRequest.Builder
 */
@RequiresApi(26)
public final @NonNull B setBackoffCriteria(
        @NonNull BackoffPolicy backoffPolicy,
        @NonNull Duration duration) {
    mBackoffCriteriaSet = true;
    mWorkSpec.backoffPolicy = backoffPolicy;
    mWorkSpec.setBackoffDelayDuration(duration.toMillis());
    return getThis();
}
  • 设置任务启动约束。假如有些日志文件上传,我要确保有网的时候进行,你可以通过 setConstraints() 来设置一些约束,没有这些限制默认是立即执行的。
/**
 * 向WorkRequest添加约束。
 * 参数:
 * 约束——工作的约束
 * 返回:
 * 当前的WorkRequest.Builder
 */
public final @NonNull B setConstraints(@NonNull Constraints constraints) {
    mWorkSpec.constraints = constraints;
    return getThis();
}

// 例:
val data = Data.Builder().put(TEST_WORKER_KEY_ID, 10010).build()
val constraints = Constraints.Builder()
    // 设备电池是否应处于可接受的水平以使 WorkRequest 运行
    .setRequiresBatteryNotLow(true)
    // 网络连接状态下执行
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()
val request = OneTimeWorkRequest.Builder(BlurWorker::class.java)
    .setInputData(data)
    .setConstraints(constraints)
    .build()
WorkManager.getInstance(this).run {
    enqueue(request)
    getWorkInfoByIdLiveData(request.id).observe(this) { workInfo ->
        when (workInfo.state) {

        }
    }
}

观察 Worker 的执行状态

  • 有的时候我们需要知道任务的执行进度,通过 WorkManager 根据 WorkRequestid 获取当前任务的状态。
val data = Data.Builder().put(TEST_WORKER_KEY_ID, 10010).build()
val request = OneTimeWorkRequest.Builder(BlurWorker::class.java)
    .setInputData(data)
    .build()
WorkManager.getInstance(this).run {
    enqueue(request)
    // 根据任务 id 获取任务状态
    getWorkInfoByIdLiveData(request.id).observe(this@WorkManagerActivity) { workInfo ->
        when (workInfo.state) {
            // 省略
        }
    }
}

image.png

顺序执行

  • 有的时候我们执行一些任务需要按照顺序执行,比方说日志文件先压缩后上传。
val reqOne =
    OneTimeWorkRequest.Builder(BlurWorkOne::class.java).setInputData(data).build()
val reqTwo =
    OneTimeWorkRequest.Builder(BlurWorkTwo::class.java).setInputData(data).build()
WorkManager.getInstance(this).beginWith(reqOne).then(reqOne).enqueue()

任务会先执行完 reqOne 接着再执行 reqTwo

image.png

WorkContinuation

  • 有的时候执行一些任务需要多个前置条件,多个前置条件执行结束才能进行下一个,这个时候我们可以通过 WorkContinuation 对任务进行处理
val data = Data.Builder().put(TEST_WORKER_KEY_ID, 10010).build()
val reqOne = OneTimeWorkRequest.Builder(BlurWorkOne::class.java).setInputData(data).build()
val reqTwo = OneTimeWorkRequest.Builder(BlurWorkTwo::class.java).setInputData(data).build()

val reqThree =
    OneTimeWorkRequest.Builder(BlurWorkThree::class.java).setInputData(data).build()
val reqFour =
    OneTimeWorkRequest.Builder(BlurWorkFour::class.java).setInputData(data).build()
    
val reqFive =
    OneTimeWorkRequest.Builder(BlurWorkFive::class.java).setInputData(data).build()
    
val combineReq = WorkManager.getInstance(this).beginWith(reqOne).then(reqTwo)
val combineReq2 = WorkManager.getInstance(this).beginWith(reqThree).then(reqFour)
WorkContinuation.combine(arrayListOf(combineReq, combineReq2)).then(reqFive).enqueue()

image.png

上面例子: Result.success(inputData) 存在相同的 key 值,ArrayCreatingInputMerger 将每个键与数组配对。如果每个键都是唯一的,您会得到一系列一元数组。

image.png 如果存在任何键冲突,那么所有对应的值会分组到一个数组中。

image.png

简单的说就是:

key 值不相同时
#reqOne
doWork(){
    Result.success(workDataOf(TEST_WORKER_KEY_CONTENT to "Result"))
}
#reqTwo
doWork(){
    Result.success(workDataOf(TEST_WORKER_KEY_CONTENT1 to "result"))
}

WorkContinuation.combine(arrayListOf(reqOne, reqTwo)).then(reqFive).enqueue()

#reqFive
doWork(){
    inputData.getString(TEST_WORKER_KEY_CONTENT)
    // 得到的结果就是 "Result"
    inputData.getString(TEST_WORKER_KEY_CONTENT1)
    // 得到的结果就是 "result"
}

key 值相同时
#reqOne
doWork(){
    Result.success(workDataOf(TEST_WORKER_KEY_CONTENT to "Result"))
}
#reqTwo
doWork(){
    Result.success(workDataOf(TEST_WORKER_KEY_CONTENT to "result"))
}

WorkContinuation.combine(arrayListOf(reqOne, reqTwo)).then(reqFive).enqueue()

#reqFive
doWork(){
    inputData.getStringArray(TEST_WORKER_KEY_CONTENT)
    // 得到的结果就是 ["Result","result"]
}

参阅文章

medium.com/androiddeve…

developer.android.com/topic/libra…

developer.android.com/guide/backg…

android-developers.googleblog.com/2018/10/mod…