查看原文
其他

Jetpack WorkManager 看这一篇就够了| 开发者说·DTalk

The following article is from Android技术圈 Author 黄林晴

本文原作者: 黄林晴原文发布于: Android技术圈


什么是 WorkManager



按照官方描述,WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。



任务类型



WorkManager 任务类型分为立即运行、长期运行和延期执行,使用方式与周期关系如下所示:

立即

一次性

OneTimeWorkRequest 和 Worker。如需处理加急工作,请对 OneTimeWorkRequest 调用 setExpedited()。

长期运行

一次性或定期

任意 WorkRequest 或 Worker。在工作器中调用 setForeground() 来处理通知。

可延期

一次性或定期

PeriodicWorkRequest 和 Worker。


接下来,来看具体的使用方法。


入门使用



添加依赖库


本文代码使用 Kotlin 编写,所以这里仅引入 Kotlin 相关的库即可,在 build.gradle 中添加代码如下所示:
def work_version = "2.7.1"implementation "androidx.work:work-runtime-ktx:$work_version"


如果使用的是 Java 语言该如何引用呢?听我的,放弃吧~


定义工作 Worker


这里我们以上传日志文件任务为例,新建 UploadLogWorker 类,继承自 Worker,代码如下所示:
class UploadLogWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result { Log.d("打印线程", Thread.currentThread().name) return Result.success() }}

继承自 Worker 的类需要重写 doWork 方法,我们可以在这个方法中执行具体的任务,这里为了有演示结果打印出线程的名称。

Result 用于返回任务的执行结果 Result.success 表示执行成功;Result.failure、Result.retry 则分别表示执行失败和失败后尝试重试。


创建任务请求 WorkRequest


这里我们创建一个一次性的执行任务,代码如下所示:
val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>().build()


将任务提交系统


创建好任务之后,就可以将任务提交系统,执行请求,代码如下所示:
WorkManager.getInstance(this).enqueue(uploadLogWorkerRequset)


运行 App,运行结果如下图所示。


为任务传递参数


许多时候我们在执行任务的时候是需要参数的,比如上传日志文件我们要知道日志文件的路径或者其他参数,我们怎么样将参数传递给 Worker 呢?

我们可以通过 WorkRequest 的 setInputData 方法来设置参数,代码如下所示:
val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>() .setInputData(workDataOf("filePath" to "file://***", "fileName" to "log.txt")) .build()


这里我们传递了文件路径 filePath 和文件名 fileName,在 Worker 通过 getInputData 方法接受,比如我们在 doWork 中接受参数并打印。代码如下所示:
override suspend fun doWork(): Result { val filePath = inputData.getString("filePath") val fileName = inputData.getString("fileName") Log.d("接受的参数", "$fileName:$filePath") return Result.retry() }


运行程序,打印如下图所示。
这样我们就完成了一个最简单的 WorkManager 使用案例。接着我们来进一步的探索。



执行加急工作所需要知道的



从 WorkManager 2.7 开始,我们可以调用 setExpedited 方法来告诉系统,我这个任务是加急任务,请尽快执行。修改代码如下所示:
val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>() .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build()

setExpedited 方法中的 OutOfQuotaPolicy 参数有两个枚举值,含义如下所示。

枚举值

含义

RUN_AS_NON_EXPEDITED_WORK_REQUEST

当系统无法为任务加急处理时,任务变成常规任务

DROP_WORK_REQUEST

当系统无法为任务加急处理时,删除该任务


所以我们这里声明为 RUN_AS_NON_EXPEDITED_WORK_REQUEST 即可。再次运行程序。

OK,完美运行???

不过我的手机是 Android 12 的,为了确保没问题,我们必须在 Android 11 或低版本上执行一次。没崩溃,但是任务却没执行,我们看到了错误日志如下图所示。
Emm.. 一堆乱七八糟的,关键信息在这句话

Expedited WorkRequests require a ListenableWorker to provide an implementation for `getFore

从官方我们获取到了这些信息: 在 Android 12 之前,工作器中的 getForegroundInfoAsync() 和 getForegroundInfo() 方法可让 WorkManager 在您调用 setExpedited() 时显示通知。如果您想要请求任务作为加急作业运行,则所有的 ListenableWorker 都必须实现 getForegroundInfo 方法。


如果未能实现对应的 getForegroundInfo 方法,那么在旧版平台上调用 setExpedited 时,可能会导致运行时崩溃。


了解到了这些,那我们就来实现 getForegroundInfo() 方法,修改 UploadLogWorker 代码如下所示:

class UploadLogWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result { Log.d("打印线程", Thread.currentThread().name) setForegroundAsync(getForegroundInfo()) return Result.success() }
@SuppressLint("RestrictedApi") override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> { val future = SettableFuture.create<ForegroundInfo>() future.set(getForegroundInfo()) return future }

fun getForegroundInfo(): ForegroundInfo { val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( "1", "hh", NotificationManager.IMPORTANCE_HIGH ) notificationManager.createNotificationChannel(channel) }
val notification = NotificationCompat.Builder(applicationContext, "1") .setSmallIcon(R.drawable.ic_launcher_background) .setContentTitle(applicationContext.getString(R.string.app_name)) .setContentText("我是一个上传日志的任务") .build() return ForegroundInfo(1337, notification) }

}


再次在 Android 11 上运行程序,发现打印出了日志,并显示了一个任务通知,如下图所示。
这一点是在执行加急工作时所必须要注意的。



协程工作 CoroutineWorker



  1. 将继承类修改为 CoroutineWorker;

  2. 实现 getForegroundInfo 方法,内容与上 getForegroundInfo 一致。



定时任务 PeriodicWorkRequest



在 3.2 中我们定义了一次性任务 OneTimeWorkRequestBuilder,现在我们将上传日志的这个任务修改为定时任务,代码如下所示:
val uploadLogWorkerRequset: WorkRequest = PeriodicWorkRequestBuilder<UploadLogWorker>(15,TimeUnit.MINUTES) .build()


这里指定了,定时任务的周期是 15 分钟一次,可以定义的最短重复间隔就是 15 分钟,这一点开发者在测试的时候需要注意,不能傻傻的等着... 这里我就傻傻的等了 15 分钟,确保定时任务是可以执行的。



工作约束、延迟执行和重试策略



工作约束


很多情况下,我们需要为任务添加工作约束,比如上传日志的任务肯定是在有网络的条件下进行的,当前支持的约束条件如下所示。

NetworkType

约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。

BatteryNotLow

如果设置为 true,那么当设备处于 "电量不足模式" 时,工作不会运行。

RequiresCharging

如果设置为 true,那么工作只能在设备充电时运行。

DeviceIdle

如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能。

StorageNotLow

如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。


比如我们现在为一次性任务添加约束为在连接 wifi 的情况下执行,首先用 Constraints 构建一个约束实例可以将多个约束条件放在一起。代码如下所示:
val constraints = Constraints.Builder() .setRequiresCharging(true) .build()


这里设置为仅在充电的时候执行。接着为任务构建器添加约束。
val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>() .setConstraints(constraints) .build()


这样一来任务就会在仅充电的时候执行了。


延迟执行


延迟执行适用于一次性任务和定时任务,但应用在定时任务时仅对第一次执行有效,为啥呢?因为是定时任务呀~

我们为一次性任务设置延迟时间为 5 秒钟,代码如下所示:
val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>() .setConstraints(constraints) .setInitialDelay(5,TimeUnit.SECONDS) .build()


运行程序,可以看到 5 秒钟后,程序才打印了日志,这里就不演示了。


重试策略


在 3.2 中定义 Work 中我们提到了 Result.retry 可以让任务重试,我们也可以自定义任务的重试策略和退避政策,我们通过具体的例子来解释。

val uploadLogWorkerRequset: WorkRequest = OneTimeWorkRequestBuilder<UploadLogWorker>() .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS ) .build()

最短退避延迟时间设置为允许的最小值,即 10 秒。由于政策为 LINEAR,每次尝试重试时,重试间隔都会增加约 10 秒。例如,第一次运行以 Result.retry() 结束并在 10 秒后重试;然后,如果工作在后续尝试后继续返回 Result.retry(),那么接下来会在 20 秒、30 秒、40 秒后重试,以此类推。

打印日志如下图所示。
我们可以看到,第一次任务失败后延迟了 10 秒重新执行,第二次延迟了 20 秒,第三次延迟了 40 秒...



观察工作执行结果



在任务完成后,我可能需要进行更新 UI 或者业务逻辑操作。我们可以通过注册监听器来观察 WorkInfo 的变化,以根据 ID 查询 WorkInfo 状态为例,代码如下所示:
WorkManager.getInstance(this).getWorkInfoByIdLiveData(uploadLogWorkerRequset.id).observe(this){ if (it.state == WorkInfo.State.SUCCEEDED){ Toast.makeText(this,"任务执行成功,更新UI",Toast.LENGTH_LONG).show() }else{ //任务失败或重试 } }


除了 getWorkInfoByIdLiveData 之外还有根据 tag、name 等查询的转化方法,这里读者可自行查看 API。

运行程序,结果如下图所示。
类似的我们还可以通过 cancelWorkById 等方法来取消任务的执行。这里不做演示了。此外还有一些其他的特性,感兴趣的读者可以自行实践。



总结



特性及注意事项


1). 在早于 Android 12 的 API 版本中,加急工作都是由前台服务执行的,而从 Android 12 开始,它们将由加急作业 (expedited job) 实现。所以在第 4 小节中,默认 Android 12 上并不会显示通知栏。

2). WorkManager 只是一个处理定时任务的工具。

3). WorkManager 最早兼容到 API 14 (Android 4.0)

4). 使用 WorkManager 注册的周期性任务不能保证一定会准时执行,这并不是 bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减少 CPU 被唤醒的次数,从而有效延长电池的使用时间

5). WorkManager 官方虽然称它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行。但是在国产手机中是不可能的,因为系统自己做了改动。但是在国产机上测试 (OPPO) 退出后,再进来也会执行之前的任务。这个时候可能就会有重复的任务执行。





长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。




 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk" 




您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存