WorkManager学习(二)

294 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

在上一篇学习笔记中已经学习了WorkManager的基本使用方式,主要包含以下几步:

  1. 定义工作器,通过继承Worker类来定义一个工作器(需要使用到Kotlin协程可以继承CoroutineWorker),重写其中的doWork()方法来实现需要执行的操作
  2. 定义WorkRequest,根据具体需要实现的工作,选择合适的WorkRequest,一般选择OneTimeWorkRequest或者PeriodicWorkRequest
  3. 定义工作执行的约束条件,通过创建Constraints对象设置当前工作运行时受到的约束,并将约束条件设置给上一步中定义的WorkRequest
  4. 通过调用setInitialDelay()设置工作延迟执行的时间
  5. 通过WorkManager.getInstance(context).enqueue(WorkRequest)将工作提交给系统,等待系统在满足条件的时候执行工作。

这篇学习笔记仍然主要来自官方文档,点击此处可以跳转到官方文档部分,主要学习以下内容:

  1. 工作的重试策略
  2. 标记工作,对工作添加TAG可以方便管理工作
  3. 设置工作的输入数据

重试

如果工作在执行过程中出现错误,并且在这种情况下我们希望能够重试工作,那么此时就可以从工作器Worker中的doWork()方法中返回Result.retry(),此时系统会根据我们配置的重试策略进行重试。重试策略有以下限制:

  • 需要设置重试的等待时间,默认为10秒,不能小于10秒,小于10秒仍然会按照10秒的间隔进行下一次重试
  • WorkManager目前提供了两种重试的策略,分别是:LINEAREXPONENTIAL

下面的代码演示了在一个工作执行失败之后进行重试:

    val workRequest = OneTimeWorkRequestBuilder<LoggerWorker>()
        .setBackoffCriteria(BackoffPolicy.LINEAR,15,TimeUnit.SECONDS)
        .build()
    //提交任务
    WorkManager.getInstance(this).enqueue(workRequest)

这里仍然使用了之前定义的一个打印日志的工作器,只不过修改它的doWork()的返回值,现在会在打印完信息之后重试10次,之后返回成功:

    override fun doWork(): Result {
        //打印日志的代码
        mRetryCount ++
        //这里返回重试
        return if(mRetryCount > 10){
            Result.success()
        }else{
            Result.retry()
        }
    }

执行上面的程序得到如下的输出:

//当重试策略设置为LINEAR的时候,输出如下:
2022-04-15 10:49:27.910 5831-5961/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 2:49:27:910
2022-04-15 10:49:42.933 5831-5962/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 2:49:42:933
2022-04-15 10:50:12.947 5831-5928/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 2:50:12:947
2022-04-15 10:50:57.999 5831-5961/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 2:50:57:999

//当重试策略设置为EXPONENTIAL的时候,输出如下:
2022-04-15 14:14:24.069 14291-14371/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 6:14:24:69
2022-04-15 14:14:39.083 14291-14375/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 6:14:39:82
2022-04-15 14:15:09.089 14291-14380/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 6:15:9:89
2022-04-15 14:16:09.163 14291-14371/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 6:16:9:163
2022-04-15 14:18:09.271 14291-14375/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 6:18:9:271
2022-04-15 14:22:09.363 14291-14380/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 6:22:9:363
2022-04-15 14:30:09.463 14291-14371/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-15 6:30:9:463

从上面打印的日志也可以看出,重试策略设置为LINEAR的时候,每次重试的间隔是上一次重试间隔的2倍,这也就是LINEAR的含义,间隔时间是线性增加的。

而重试策略设置为EXPONENTIAL的时候,当前重试的间隔时间为t = 15 * 2^n,其中15是设置的间隔时间,n表示当前的重试次数,n从0开始,间隔时间是呈指数级增加的。

标记工作

每个工作请求(WorkRequest)都有一个唯一标识符,该标识符可以用于在后面取消工作或者观察进度。

如果有很多工作,这些工作可以按照某一个类型进行分组,那么我们可以通过对这些工作进行分组以便于对同一个分组下的工作进行统一的管理。对工作进行标记也就是对工作进行分组,可以通过调用WorkRequestaddTag(String tag)方法对工作进行标记和分组。

对工作进行标记和分组之后,我们就可以通过添加的tag标记来对某一个分组的工作进行管理,例如调用WorkManager.cancelAllWorkByTag(String tag)取消带有指定tag的工作,或者通过调用WorkManager.getWorkInfoByTag(String tag)来获取指定tag的工作的信息WorkInfo,如下所示:

    //创建一个工作并对其添加标记
    val workRequest = OneTimeWorkRequestBuilder<LoggerWorker>()
        .addTag(WORKER_TAG)
        .build()
    //提交任务
    WorkManager.getInstance(this).enqueue(workRequest)

同时,也可以对一个工作添加多个标记,可以通过WorkInfo.getTags获取对指定工作获取的所有标记:

    //在Worker中获取当前工作的tag
    Log.i(TAG,"current work tags is:${tags}")

在外部获取同一个tag的工作信息WorkInfo,以及通过tag取消工作:

    //获取指定标记的工作
    val workers = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG)
    Log.i(TAG,"worker is:${workers.get()}")
    //根据标记取消所有的工作
    WorkManager.getInstance(this).cancelAllWorkByTag(WORKER_TAG)

上面的程序输出如下:

current work tags is:[WorkManagerStudy_1_TAG, com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker]
worker is:[WorkInfo{mId='0993dd1f-f18c-4d84-9d19-46bc1ea30290', mState=ENQUEUED, mOutputData=Data {}, mTags=[WorkManagerStudy_1_TAG, com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker], mProgress=Data {}}, WorkInfo{mId='204b5afb-4150-469d-805b-ff9afe11ff6d', mState...

输入数据

很多时候执行工作需要依赖外部的数据才可以,比如在上一篇笔记中,下载启动页的图片,上一篇笔记中是下载一个固定地址的图片,但是实际项目中应该是下载后台配置的图片,但是这个图片地址可能并不在一个单独的接口中(如果在一个单独的接口中,我们完全可以在工作中去获取这个接口的数据),可能是在别的接口中,比如在用户登录接口中下发,这个时候执行下载图片工作的时候就需要外部将图片的地址传递进去,然后才能开始下载图片。

当需要从外部传递数据到Worker工作器中时,可以通过调用WorkRequest.BuildersetInputData(Data)方法设置需要传递给Worker数据,数据以键值对的形式保存,在Worker中,可以通过getInputData()获得数据,然后执行后续的操作。

    val data = Data.Builder()
        .putBoolean("isRetry",true)
        .putInt("retryCount",10)
        .putString("url","xxx")
        //查看put方法的签名可以知道,这个方法只能接受基本类型和基本类型的数组,字符串以及字符串数组,像下面这种数据对象是不可以的
        //.put("object",TestEntity("testKey","testValue"))
        .build()

    val workRequest = OneTimeWorkRequestBuilder<LoggerWorker>()
        .addTag(WORKER_TAG)
        .setInputData(data)
        .build()

    WorkManager.getInstance(this).enqueue(workRequest)

Worker中获取上面添加的数据:

    //获取外部传递的数据
    val retry = inputData.getBoolean("isRetry",false)
    val count = inputData.getInt("retryCount",-1)
    val url = inputData.getString("url")
    Log.i(TAG,"inputData:$retry,$count,$url")

上面的程序运行后输出如下:

    inputData:true,10,xxx

唯一工作

不管是一次执行的任务,还是重复执行的任务,在将工作加入到执行队列的时候都必须小心,避免重复。比如我们可能希望在每天的下午某个时间提醒我们打下班卡,我们希望这是一个每天都会执行的重复工作,在将这项工作加入到队列中的时候,我们就必须小心一点,今亮避免重复将该任务加入到队列中,为了实现这样的功能,我们可以将工作设置为唯一工作。

唯一工作既可以用于一次性工作,也可以用于重复性工作,对于一次性工作,可以使用WorkManager.enqueueUniqueWork()进行调度,而对于重复性工作,可以使用WorkManager.enqueueUniquePeriodicWork()进行调度。

这两个方法都可以接受以下参数:

  • uniqueWorkName: 用于标识唯一工作请求的String
  • existingWorkPolicy: 这个参数应用告知WorkManager,如果已经存在一个同一个名称的工作,应该执行什么样的操作
  • work: 需要调度的工作

下面的代码演示了将一个一次性工作调度为唯一工作:

    class SecondWorker(private val mContext: Context,private val mWorkParams: WorkerParameters): Worker(
        mContext,mWorkParams
    ){
        companion object{
            private const val  TAG = "SecondWorker"
        }

        override fun doWork(): Result {
            Log.i(TAG,"start work ...")
            //等待10秒钟,模拟工作正在执行
            Thread.sleep(10 * 1000)
            Log.i(TAG,"end work ...")

            return Result.success()
        }
    }
    
    
    //将一个一次性工作调度为唯一工作
    val workRequest = OneTimeWorkRequestBuilder<SecondWorker>()
        .addTag(WORKER_TAG)
        .build()
    //调度为唯一工作
    WorkManager.getInstance(this).enqueueUniqueWork(
        "secondWork",
        ExistingWorkPolicy.KEEP,
        workRequest
    )

上面的代码中定义了一个工作器,在其中我们让线程等待10秒模拟工作的执行。之后将这个任务设置为一次性任务,然后在调度的时候设置为唯一任务,其中我们的拒绝策略为ExistingWorkPolicy.KEEP,这标识如果是同一个任务,则会丢弃新的任务而仍然使用已经注册的任务,运行上面的代码,可以得到如下的数据出:

2022-04-28 15:22:53.291 3394-3596/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 15:23:03.292 3394-3596/com.project.mystudyproject I/SecondWorker: end work ...

在任务执行期间,尽管我们一点在点击按钮尝试添加新的任务,但是仍然不会有新的任务被添加进去。

上面我们的拒绝策略是ExistingWorkPolicy.KEEP,下面演示了其余的拒绝策略:

//ExistingWorkPolicy.REPLACE
2022-04-28 17:36:44.789 6224-6224/com.project.mystudyproject I/MainActivity: initUI...
2022-04-28 17:36:51.416 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: start build SecondWork
2022-04-28 17:36:51.416 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: enqueue SecondWork complete
2022-04-28 17:36:51.443 6224-6294/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 17:36:52.017 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: start build SecondWork
2022-04-28 17:36:52.017 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: enqueue SecondWork complete
2022-04-28 17:36:52.021 6224-6250/com.project.mystudyproject I/SecondWorker: stop this
2022-04-28 17:36:52.057 6224-6295/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 17:36:52.200 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: start build SecondWork
2022-04-28 17:36:52.200 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: enqueue SecondWork complete
2022-04-28 17:36:52.206 6224-6246/com.project.mystudyproject I/SecondWorker: stop this
2022-04-28 17:36:52.241 6224-6296/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 17:36:52.384 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: start build SecondWork
2022-04-28 17:36:52.384 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: enqueue SecondWork complete
2022-04-28 17:36:52.392 6224-6250/com.project.mystudyproject I/SecondWorker: stop this
2022-04-28 17:36:52.550 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: start build SecondWork
2022-04-28 17:36:52.550 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: enqueue SecondWork complete
2022-04-28 17:36:52.558 6224-6260/com.project.mystudyproject I/SecondWorker: stop this
2022-04-28 17:36:52.715 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: start build SecondWork
2022-04-28 17:36:52.716 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: enqueue SecondWork complete
2022-04-28 17:36:52.722 6224-6246/com.project.mystudyproject I/SecondWorker: stop this
2022-04-28 17:36:52.869 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: start build SecondWork
2022-04-28 17:36:52.869 6224-6224/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: enqueue SecondWork complete
2022-04-28 17:36:52.876 6224-6250/com.project.mystudyproject I/SecondWorker: stop this
2022-04-28 17:37:01.444 6224-6294/com.project.mystudyproject I/SecondWorker: end work ...
2022-04-28 17:37:01.444 6224-6294/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 17:37:02.059 6224-6295/com.project.mystudyproject I/SecondWorker: end work ...
2022-04-28 17:37:02.059 6224-6295/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 17:37:02.241 6224-6296/com.project.mystudyproject I/SecondWorker: end work ...
2022-04-28 17:37:02.241 6224-6296/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 17:37:11.445 6224-6294/com.project.mystudyproject I/SecondWorker: end work ...
2022-04-28 17:37:11.445 6224-6294/com.project.mystudyproject I/SecondWorker: start work ...
2022-04-28 17:37:12.060 6224-6295/com.project.mystudyproject I/SecondWorker: end work ...
2022-04-28 17:37:12.242 6224-6296/com.project.mystudyproject I/SecondWorker: end work ...

可以看到,当我们不断添加同一个Worker的时候,REPLACE的策略是执行新的Worker之前会结束上一次的Worker,然后执行新的Worker。需要注意的是,上面是日志中仍然打印了多次end work,看起来似乎是没有结束Worker,其实并非如此,如何结束Worker应该是由我们来操作的,结束任务并不意味着中断线程,这里分为两部分:

  • 对于长时间执行的任务,应该经常检查isStopped的属性是否为true,如果为true则应该主动停止工作
  • 另外当任务被关闭的时候会回调onStopped()方法,我们可以在这个方法中做一些释放资源的操作

下面的代码演示了在Worker中执行工作的时候的某个时刻检查是否停止并做出不同的操作,这里只是简单打印日志,并没有设置线程中断等操作:

if(!isStopped) {
    Log.i(TAG, "end work ...")
}else{
    return Result.failure()
}

如果我们希望在接收到停止操作之后能够立即结束执行任务的线程,则可以像如下的方式进行操作:

    class SecondWorker(private val mContext: Context,private val mWorkParams: WorkerParameters): Worker(
        mContext,mWorkParams
    ){
        companion object{
            private const val  TAG = "SecondWorker"
        }

        var mWorkThread: Thread? = null

        override fun doWork(): Result {
            Log.i(TAG,"start work ...")
            mWorkThread = Thread.currentThread()
            try{
                Thread.sleep(10 * 1000)
                Log.i(TAG,"end work ... ${Thread.currentThread()} -- $mWorkThread")
            }catch (e: InterruptedException){
                Log.i(TAG,"error: $e")
                return Result.failure()
            }
            return Result.success()
        }

        override fun onStopped() {
            super.onStopped()
            mWorkThread?.interrupt()
        }
    }