WorkManager学习

206 阅读6分钟

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

概述

WorkManager是适合用于持久性工作的推荐解决方案,如果工作需要在应用退出或者系统重启之后仍然可以运行,便是永久性的工作。由于大多数后台操作都是通过持久性工作完成的,因此WorkManager主要适用于后台处理操作。

WorkManager可处理三种类型的持久性工作,分别是:

  • 立即执行: 必须立即开始且很快就能完成的任务,可以加急;
  • 长时间运行: 运行时间可能较长(超过10分钟)的任务;
  • 可延期执行: 延期开始并且可以定期运行的预定义任务。

针对不同的工作类型,下表列出了简单的操作类:

类型周期使用方式
立即一次性OneTimeWorkRequestWorker
如需处理加急工作,可以对OneTimeWorkRequest调用setExpedited()
长期运行一次或定期任意WorkRequestWorker
在工作器中调用setForeground()来处理通知
可延期一次性或定期PeriodicWorkRequestWorker

开始使用

首先需要将WorkManager的相关依赖导入到项目中,对依赖的导入可以参考这篇文章,我在项目中只导入了基础的组件:

implementation "androidx.work:work-runtime-ktx:$work_version"

定义工作

通过继承Worker类来定义一个工作。其中doWork()方法在WorkManager中提供的后台线程上异步执行。

继承Worker之后,我们可以通过重写doWork()方法实现自己的工作,假设我们只是在工作中打印一行数据:

    /**
     * 一个简单的打印一行日志的work
     */
    class LoggerWorker(private val context: Context,private val workParams: WorkerParameters): Worker(context,workParams){

        private val TAG = LoggerWorker::class.java.simpleName

        //子类必须重写此方法
        override fun doWork(): Result {
            //获取当前时间并打印出来
            val calendar = Calendar.getInstance()
            val timeLogger = StringBuilder(context.getString(R.string.current_time)).apply {
                this.append(calendar.get(Calendar.YEAR))
                this.append("-")
                this.append(calendar.get(Calendar.MONTH) + 1)
                this.append("-")
                this.append(calendar.get(Calendar.DAY_OF_MONTH))
                this.append(" ")
                this.append(calendar.get(Calendar.HOUR))
                this.append(":")
                this.append(calendar.get(Calendar.MINUTE))
                this.append(":")
                this.append(calendar.get(Calendar.SECOND))
                this.append(":")
                this.append(calendar.get(Calendar.MILLISECOND))
            }
            Log.i(TAG,timeLogger.toString())
            
            //最终的结果返回成功
            return Result.success()
        }

    }

可以看到,定义一个Worker是十分简单的:

  • 继承Worker
  • 重写其中的doWork()方法,这个方法中就是我们要做的具体的事情
  • 根据任务的状态返回当前任务是否成功等。

创建WorkRequest

通过继承Worker可以定义我们的任务需要执行什么东西,而WorkerRequest则定义了我们的任务将如何执行,在什么样的情况下以什么样的方式运行。

通过之前的概述中的学习,我们已经了解了WorkRequest包含多种类型,我们可以根据自己的工作需要使用相应的工作类型,下面的代码中我们使用OneTimeWorkRequest来让上面定义的LoggerWorker执行一次,代码如下所示:

    val workRequest = OneTimeWorkRequestBuilder<LoggerWorker>()
        .build()

通过上面简单的一行代码我们就对工作如何执行进行了定义,当然上面并没有设置任何的特殊属性,所以我们定义的工作将会以默认的方式去执行。

通过OneTimeWorkRequestBuilder<reified W : ListenableWorker>可以定义一次性任务的执行参数,其中泛型参数要求我们传递ListenableWorker及其子类,通过查看类的继承关系可以知道,Worker类就是继承自ListenableWorker,因此这里我们只需要传递我们在之前已经定义好的LoggerWorker即可。

WorkerRequest提交给系统

经过上面两步,我们分别定义了需要执行的工作LoggerWorker,以及工作如何执行WorkRequest。但是这样任务还不能执行,还需要最后一步:将任务提交给系统,然后等待任务执行即可,我们可以使用WorkManager提供的enquene()方法将工作提交给系统.如下代码所示:

    //将任务提交给系统
    WorkManager.getInstance(this)
        .enqueue(workRequest)

经过上面三步,我们就将任务成功进行了包装并提交给系统,运行上面的程序将会得到如下输出:

2022-04-06 20:11:01.141 10291-10291/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: commit work start
2022-04-06 20:11:01.154 10291-10291/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: commit work end
2022-04-06 20:11:01.182 10291-10438/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-6 8:11:1:182
2022-04-06 20:11:01.183 10291-10405/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=f70cd76c-fd8c-4248-88dc-582d53923d63, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]

可以看到:成功执行了我们打印日志的信息。其中最后一行输出是系统打印的,通过最后一行输出也可以看出我们任务的相关信息。

WorkRequest

在上面的学习中,我们定义了一次执行一次打印工作的WorkRquest,通过查看WorkRequest的定义可以知道,它本身是一个抽象类,官方文档也明确给出,我们应该使用其中的两个子类OneTimeWorkRequest或者PeriodicWorkRequest这两个派生类。其中OneTimeWorkRequest适合调度一次执行的非重复性的工作,PeriodicWorkRequest适合调度以一定的时间间隔重复执行的工作。

调度一次性工作

我们已经知道,对于一次性工作,我们需要使用OneTimeWorkRequest,如果我们的工作无需额外的配置,使用系统默认的配置即可,那么我们可以使用OneTimeRequest.from()来声明一个WorkRequest,然后去执行,如下所示:

    val workRequest = OneTimeWorkRequest.from(LoggerWorker::class.java)
    //将任务提交给系统
    WorkManager.getInstance(this).enqueue(workRequest)

我们仍然使用之前定义的打印日志的任务,运行上面的程序,输出结果如下:

2022-04-06 20:20:53.616 10820-10894/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-6 8:20:53:616
2022-04-06 20:20:53.618 10820-10859/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=1280023f-4dd0-4d21-8dc5-f40fc1d5bc15, tags={ com.project.mystudyproject.wo

如果我们需要对任务添加一些配置信息,从而保证任务在指定条件下才会执行,那么我们需要和之前一样使用OneTimeWorkRequest.Builder来创建对应的WorkRequest,如下所示:

    Log.i(TAG,"commit work")
    val workRequest = OneTimeWorkRequestBuilder<LoggerWorker>()
        .setConstraints(
            Constraints.Builder()
                    //设置需要当前网络属于需要计量的网络连接
                .setRequiredNetworkType(NetworkType.METERED)
                .build()
        )
    .build()
    //将任务添加到系统中
    WorkManager.getInstance(this).enqueue(workRequest)

在上面的代码中,我们通过OneTimeWorkRequest.Builder创建了一个一次性调度的任务,并且设置了只有当当前网络连接数据按流量计费的网络的时候才能执行这个任务。为了验证上面设置的条件生效,可以首先断开流量连接,则会发现上面的任务不会执行。当我们重新连接到流量网络的时候,系统会自动执行添加进去的任务,如下所示:

2022-04-06 20:40:09.810 11647-11647/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: commit work
2022-04-06 20:40:10.479 11647-11647/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: commit work
2022-04-06 20:40:10.877 11647-11647/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: commit work

2022-04-06 20:40:19.076 11647-11743/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-6 8:40:19:75
2022-04-06 20:40:19.094 11647-11745/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-6 8:40:19:94
2022-04-06 20:40:19.103 11647-11678/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=577a7215-1deb-4734-a7e3-c8bdee45090d, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]
2022-04-06 20:40:19.108 11647-11747/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-6 8:40:19:107
2022-04-06 20:40:19.119 11647-11674/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=683feec6-72b7-4e6a-8c73-b6a1f31ca377, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]
2022-04-06 20:40:19.140 11647-11678/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=b70228e5-f0ba-4ced-93dd-ddae79826092, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]

首先我们在断开流量网络的情况下,点击了三次按钮,打印了三次commit work,之后虽然将任务提交给了系统,但是由于不满足条件,所以任务不会执行,接着我们重新连接了流量网络,发现我们刚才提交了三次的任务自动执行了。

调度定期工作

有时候我们的应用需要定期执行某些工作,比如定期上传日志,定期更新缓存等操作,我们已经知道,可以通过PeriodicWorkRequest创建定期执行的WorkRequest

    val workRequest =  PeriodicWorkRequestBuilder<LoggerWorker>(15.toLong(),TimeUnit.MINUTES)
        .build()
    //提交任务
    WorkManager.getInstance(this).enqueue(workRequest)

在上面的代码中,我们创建了一个间隔15分钟执行一次的任务,这个时间是定期工作的任务间隔的最小时间。当我们首次提交这个任务的时候,这个任务就会立即开始执行一次,接下来我们就需要等待下一次执行任务的时间,判断是否能够按照我们的要求在下个时间点成功执行任务。

2022-04-07 00:16:22.281 12711-12875/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-7 0:16:22:281
2022-04-07 00:16:22.282 12711-12743/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=7fdb6646-0a9e-426c-ae5e-861e758332c3, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]
2022-04-07 00:31:22.388 12711-12957/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-7 0:31:22:388
2022-04-07 00:31:22.389 12711-12743/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=7fdb6646-0a9e-426c-ae5e-861e758332c3, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]

从打印的数据可以看出:我们点击了按钮之后就立即执行了一次打印数据,之后间隔15分钟又打印了一次数据。

除了上面的配置方式,我们还可以将PeriodicWorkRequest配置为在每个时间间隔的灵活时间段内运行。查看PeriodicWorkRequest的构造方法,可以发现,它还有一个可接受四个参数的构造方法,我们可以分别指定任务执行的间隔时间以及弹性时间,也就是在间隔时间内灵活执行的时间。

    val workRequest = PeriodicWorkRequestBuilder<LoggerWorker>(
        15.toLong(),
        TimeUnit.MINUTES,
        5.toLong(),
        TimeUnit.MINUTES
    )
        .build()
    //提交任务
    WorkManager.getInstance(this).enqueue(workRequest)     

上面的代码中,我们创建了一个间隔15分钟执行一次的任务,同时这个任务在每次间隔的最后5分钟内执行,运行上面的代码我们可以得到如下的日志:

2022-04-07 02:30:42.935 13912-13912/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: commit work
2022-04-07 02:40:49.199 13912-14102/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-7 2:40:49:198
2022-04-07 02:40:49.201 13912-13935/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=1f873a9d-dad3-4519-a6f7-94c0dfc3774c, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]
2022-04-07 02:55:49.206 13912-14162/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-7 2:55:49:206
2022-04-07 02:55:49.207 13912-13935/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=1f873a9d-dad3-4519-a6f7-94c0dfc3774c, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]

通过上面的日志可以看出,我们一开始提交了任务并不会执行,而是会在第一个15分钟最后的5分钟的时间点开始执行,时间上来说就是在commit task之后的十分钟执行这个任务,之后会在下一个15分钟的后5分钟的时间点重复执行这个任务。

工作约束

工作约束可以设置工作应该在什么情况下执行,这些约束适用于WorkManager

约束类型说明
NetworkType约束工作运行时的网络类型,例如只能在wifi或者流量网络下执行
BatteryNotLow这个值设置为true,那么当设备处于低电量模式的时候不会执行相应的任务
RequiresCharging这个值设置为true,那么只有当设备正在充电的时候才会执行相应的任务
DeviceIdle这个值设置为true,那么只有当设置处于空闲状态的时候才会运行相应的任务。运行批量操作的时候应该设置这个值,因为批量操作可能会降低用户设备上正在运行的其它应用的性能
StorageNotLow这个值设置为true,那么当用户设备上的存储空间不足的时候不会执行相应的任务

当需要对某一项工作设置约束的时候,可以使用Constraints.Builder来构建一组约束,然后将这一组约束传递给WorkRequest.Builder

    //首先创建任务的约束信息
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)
        .setRequiresBatteryNotLow(true)
        .setRequiresCharging(true)
        .build()

    //构建任务请求
    val workRequest = OneTimeWorkRequestBuilder<LoggerWorker>()
        .setConstraints(constraints)
        .build()
    //提交任务
    WorkManager.getInstance(this).enqueue(workRequest)

在上面的代码中,通过设置任务执行的约束 -- 连接wifi,设备电量充足并且正在充电的情况下才会执行任务,通过在模拟器中调节参数,会得到如下的日志:

2022-04-07 03:17:14.363 14319-14686/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-7 3:17:14:363
2022-04-07 03:17:14.368 14319-14498/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=2491edab-9833-4e5b-83ee-2fed4150f058, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]

延迟工作

如果我们没有对任务设置任何的约束,或者任务在一开始的时候约束就得到了满足,那么系统将会立即执行任务,而有时候我们希望能够延迟一段时间再执行任务,这个时候就可以对WorkRequest设置延迟时间,如下所示:

    Log.i(TAG,"commit work")
    val workRequest = OneTimeWorkRequestBuilder<LoggerWorker>()
        .setInitialDelay(1,TimeUnit.MINUTES)
        .build()
    WorkManager.getInstance(this).enqueue(workRequest)

在上面的代码中,设置了一个任务并且在等待一分钟之后开始执行这个任务,日志打印如下:

2022-04-07 03:38:15.098 15438-15438/com.project.mystudyproject I/work_manager.study1.WorkManagerStudy1Activity: commit work
2022-04-07 03:39:15.126 15438-15525/com.project.mystudyproject I/LoggerWorker: 当前时间是:2022-4-7 3:39:15:126
2022-04-07 03:39:15.128 15438-15501/com.project.mystudyproject I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=c042c7a6-4d85-4a97-b939-5d919d9d4c3f, tags={ com.project.mystudyproject.work_manager.study1.WorkManagerStudy1Activity$LoggerWorker } ]

项目使用

经过了上面对于WorkManager的基本学习,我们已经能够大致了解WorkManager能够做什么用,在日常的项目中很多功能都适合使用WorkManager去做。

开屏广告

现在很多应用都有在打开的时候显示广告的页面,等待几秒钟之后才能进入到首页。一般情况下这个页面可能会显示图片甚至是视频,如果在打开这个页面的时候再去网络请求图片或者去下载视频,必然导致用户等待的时候更久,所以一般情况下我们会在第一次打开的时候不显示广告,进入到首页之后再去下载下一次要显示的广告信息,将视频或者图片缓存在本地,下一次就可以直接从本地加载图片和视频。这个功能使用WorkManager实现则更加合适,如下所示:

  1. 首先我们在启动页中尝试读取保存在本地的图片文件,如果能够获取到图片文件则使用获取到的图片文件,否则设置一个默认的启动图:
    //读取本地的图片文件
    private suspend fun loadImage() {
        withContext(Dispatchers.Default) {
            val imageDirPath = "${this@SplashActivity.cacheDir}/splash/"
            val dirFile = File(imageDirPath)
            if (dirFile.exists()) {
                val files = dirFile.listFiles()
                if (files != null && files.isNotEmpty()) {
                    var findImage = false
                    files.asSequence()
                        .filter {
                            val fileName = it.name
                            fileName.endsWith(".jpg") || fileName.endsWith(".png")
                        }
                        .firstOrNull {
                            Log.i(TAG, "first is:${it.name}")
                            val bitmap = BitmapFactory.decodeStream(it.inputStream())
                            withContext(Dispatchers.Main) {
                                mBinding.ivBg.scaleType = ImageView.ScaleType.CENTER_CROP
                                mBinding.ivBg.setImageBitmap(bitmap)
                            }
                            findImage = true
                            true
                        }
                    if(!findImage) {
                       setDefaultImage()
                    }
                } else {
                    setDefaultImage()
                }
            } else {
                setDefaultImage()
            }
            //等待3秒钟进入首页
            delay(3000)
            startActivity(Intent(this@SplashActivity, MainActivity::class.java))
            finish()
        }
    }
    
    private suspend fun setDefaultImage() = withContext(Dispatchers.Main){
        mBinding.ivBg.scaleType = ImageView.ScaleType.CENTER
        mBinding.ivBg.setImageResource(R.mipmap.ic_launcher)
    }
  1. 启动到主页之后,我们设置在连接wifi,设备电量充足的情况下去下载一张网络图片:
        //创建一个任务并在一分钟后开始下载图片
        val constraints = Constraints.Builder()
            //wifi环境下才能下载图片
            .setRequiredNetworkType(NetworkType.UNMETERED)
            //电量充足的时候下载
            .setRequiresBatteryNotLow(true)
            .build()
        
        val workRequest = OneTimeWorkRequestBuilder<LoadSplashImageWorker>()
            .setConstraints(constraints)
            //等待一分钟后开始下载图片
            .setInitialDelay(1, TimeUnit.MINUTES)
            .build()
        //提交任务
        WorkManager.getInstance(this).enqueue(workRequest)
  1. 在任务中通过HttpUrlConnection去下载一张图片并保存到本地:
    class LoadSplashImageWorker(private val context: Context, params: WorkerParameters) :
        CoroutineWorker(context, params) {

        private val TAG = LoadSplashImageWorker::class.java.simpleName

        override suspend fun doWork(): Result {
            return if (loadImage()) {
                 Result.success()
            } else {
                Result.failure()
            }
        }
        private suspend fun loadImage(): Boolean {
            Log.i(TAG, "loadImage...")
            return withContext(Dispatchers.IO) {
                //下载网络图片并保存到本地
                try {
                    val imageUrl =
                        URL("图片地址")
                    val urlConnection: HttpURLConnection =
                        imageUrl.openConnection() as HttpURLConnection
                    urlConnection.requestMethod = "GET"
                    urlConnection.doInput = true
                    urlConnection.allowUserInteraction = true
                    urlConnection.connect()
                    Log.i(TAG, "url connection:${urlConnection.content}")
                    val inputStream = urlConnection.getInputStream()
                    inputStream?.let {
                        //将图片保存到本地缓存中
                        val dirPath = "${context.cacheDir}/splash/"
                        val dirFile = File(dirPath)
                        if (!dirFile.exists()) {
                            dirFile.mkdir()
                        }
                        val imageFile = File("${dirPath}image_splash.jpg")
                        val outputStream = imageFile.outputStream()
                        outputStream.write(it.readBytes())
                        outputStream.flush()
                        outputStream.close()
                        it.close()
                    }
                    urlConnection.disconnect()
                    true
                } catch (e: Exception) {
                    Log.e(TAG, Log.getStackTraceString(e))
                    false
                }
            }
        }

这样,当我们成功将图片下载完成并保存到本地之后,下一次打开应用就可以看到我们下载的图片被设置上去了。当然这里每次都去下载图片是不合适的,正常情况下不应该重复下载同一张图片。