Jetpack系列(四):精通WorkManager,让后台任务不再失控

前言

很早之前,Android 系统的后台功能非常开放,Service 的优先级很高(仅次于前台 Activity)。但由于后台功能太过开放,太多应用使用它来抢占后台资源,严重影响了用户的体验和电池续航。

为了解决这些问题,Android 系统每更新一个版本,都会收缩后台的权限。例如:从 4.4 系统开始,AlarmManager 的触发时间由原来的精准变为不精准,5.0 系统中加入了 JobScheduler 来统一管理后台任务等。如此频繁的 API 变更和不同系统版本之间的行为差异,一时让开发者无所适从。

为此,Google 推出了 WorkManager 组件。它是一个强大且向后兼容的后台任务调度库,旨在统一后台 API。它会根据系统版本和应用状态自动选择底层是使用 AlarmManager 还是 JobScheduler。它不仅可用于处理需要延迟执行或周期性执行的任务,还支持约束执行、链式任务等功能。

注意:Service 与 WorkManager 的定位不同。Service 是 Android 系统的四大组件之一,主要用于需要立即或持续 在后台运行的任务(如音乐播放),只要不被销毁会一直在后台运行。而 WorkManager 只是一个处理可延时任务的工具,它可以保证即使应用退出或是手机重启后,任务依然会得到执行。WorkManager 适合执行定期同步服务器、上传日志等这类不要求实时完成的任务。

WorkManager 的基本用法

添加依赖

我们首先要在 build.gradle.kts 配置文件中添加如下依赖:

kotlin 复制代码
dependencies {
    // WorkManager 运行时库
    implementation("androidx.work:work-runtime:2.10.2")
    
    
    // 对于Kotlin项目,推荐直接使用ktx版本,因为它已经包含了基础库并提供协程支持
    implementation("androidx.work:work-runtime-ktx:2.10.2")
}

定义后台任务 (Worker)

创建 SimpleWorker 类,继承自 Worker 类。

kotlin 复制代码
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

    // 重写 doWork() 方法
    override fun doWork(): Result {
        // 实现任务逻辑:模拟一个耗时任务
        try {
            Log.d("SimpleWorker", "任务开始执行...")
            Thread.sleep(3000)
            Log.d("SimpleWorker", "任务执行完毕!")
            return Result.success()
        } catch (e: Exception) {
            Log.e("SimpleWorker", "任务执行失败", e)
            return Result.failure()
        }
    }
}

或是继承自 CoroutineWorker 类(支持协程),这样我们能够直接在 doWork 方法中调用挂起函数。

kotlin 复制代码
class SimpleCoroutineWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

    // 重写 doWork() 方法
    override suspend fun doWork(): Result {
        // 切换到 IO 线程执行耗时操作
        return withContext(Dispatchers.IO) {
            try {
                // 模拟一个耗时任务
                Log.d("SimpleWorker", "任务开始执行...")
                delay(3000)
                Log.d("SimpleWorker", "任务执行完毕!")

                Result.success()
            } catch (e: Exception) {
                Log.e("SimpleWorker", "任务执行失败", e)

                Result.failure()
            }
        }
    }
}

doWork() 方法并不会运行在主线程中,而是运行在由 WorkManager 管理的后台线程上,我们可以在这放心地执行耗时操作。

方法的返回值类型为 Result,表示任务的运行结果。我们可以返回 Result.success() 表示任务成功完成;返回 Result.failure() 表示任务执行失败;返回 Result.retry() 表示任务暂时遇到问题,希望 WorkManager 可以通过退避策略进行重试(待会会讲到)。

配置并创建后台任务请求 (WorkRequest)

WorkRequest 定义了要执行哪个任务以及它该如何运行。它有两个子类,分别是 OneTimeWorkRequestPeriodicWorkRequest

OneTimeWorkRequest 用于构建只执行一次的后台任务请求,PeriodicWorkRequest 则是用于构建周期性执行的后台任务请求。为了系统性能,运行周期不能低于 15 分钟。例如:

kotlin 复制代码
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    .build()

val request = PeriodicWorkRequest.Builder(
    SimpleWorker::class.java,
    15, // 重复运行的周期时长
    TimeUnit.MINUTES // 单位:分钟
).build()

WorkManager 的强大在于,我们还能为任务添加 Constraints(约束),让任务只有在满足某些条件的情况下才能运行,这是省电和智能的关键。例如:

kotlin 复制代码
// 创建约束,要求设备在电量充足且连接到Wi-Fi的时候才执行
val constraints = Constraints.Builder()
    .setRequiresBatteryNotLow(true)
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .build()

// 创建一个单次运行的后台任务请求,并应用约束
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    .setConstraints(constraints)
    .build()

将任务请求加入队列

通过 WorkManagerenqueue() 方法将 WorkRequest 任务请求交给 WorkManager 系统服务,系统会在合适的时间执行。

kotlin 复制代码
// 在 Activity, Fragment 或 ViewModel 中
WorkManager.getInstance(applicationContext).enqueue(request)

这里不能传入 Activity 或 Fragment 的 Context,会导致内存泄漏。

因为 WorkManager 的生命周期长于 Activity,如果 WorkManager 持有了 Activity 对象的引用,那么当 Activity 销毁时,Android 的垃圾回收器无法回收此 Activity 对象,该对象就留在了内存中,造成了内存泄露。而 ApplicationContext 应用上下文的生命周期和 WorkManager 匹配,都只会在应用进程被销毁时才销毁,也就不用担心内存泄露问题了。

测试

activity_main.xml 布局中新增一个按钮:

xml 复制代码
<Button
    android:id="@+id/doWorkBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Do Work" />

MainActivityonCreate() 方法中添加如下内容:

kotlin 复制代码
// 在 onCreate 方法中
binding.doWorkBtn.setOnClickListener {
    val constraints = Constraints.Builder()
                    .setRequiresBatteryNotLow(true)
                    .build()
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
        .setConstraints(constraints)
        .build()
    WorkManager.getInstance(applicationContext).enqueue(request)
}

运行程序并点击按钮,只要设备不是低电量,我们就可以在日志中看到:

less 复制代码
D/SimpleWorker    com.example.jetpacktest    任务开始执行...
D/SimpleWorker    com.example.jetpacktest    任务执行完毕!

使用 WorkManager 处理复杂的任务场景

学完了基本用法,我们来看看 WorkManager 更多强大的功能。

延迟执行、标记与取消

如果要让后台任务在指定的延时时间后执行,只需使用 setInitialDelay() 方法即可。

kotlin 复制代码
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    .setInitialDelay(
        5, // 延时时长
        TimeUnit.MINUTES // 时间单位为分钟
    )
    .build()

我们可以给后台任务请求添加标签,只需使用 addTag() 方法即可。

kotlin 复制代码
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    .addTag("data_sync")
    .build()

这样我们可以通过标签来批量取消任务,所有同一标签的任务都会被取消。

kotlin 复制代码
WorkManager.getInstance(applicationContext).cancelAllWorkByTag("data_sync")

当然你也可以根据后台任务请求的 id 来取消任务:

kotlin 复制代码
WorkManager.getInstance(applicationContext).cancelWorkById(request.id)

另外,我们可以一次性取消所有后台任务请求:

kotlin 复制代码
WorkManager.getInstance(applicationContext).cancelAllWork()

任务的输入与输出

任务往往需要外部数据才能工作,其执行结果也往往需要被后续任务使用。我们可以使用 Data 对象来在任务间传递简单数据,Data 是一个键值对容器。

首先,在创建后台任务请求时,构建好 inputData 输入数据,并通过 setInputData() 方法设置到任务请求中。

kotlin 复制代码
// 创建包含输入数据的 Data 对象
val inputData = workDataOf("USER_ID" to "12345", "SYNC_MODE" to "AUTO")

// 设置输入数据 
val requestWithData = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    .setInputData(inputData)
    .build()

然后,在后台任务的 doWork() 方法中,通过 inputData 属性获取输入数据即可。任务成功时,可以通过 Result.success(outputData) 返回 outputData 输出数据。

kotlin 复制代码
override suspend fun doWork(): Result {
    val userId = inputData.getString("USER_ID") ?: return Result.failure()
    Log.d("SimpleWorker", "正在为用户 $userId 同步数据...")

    // 创建包含输出数据的 Data 对象并返回
    val outputData = workDataOf("SYNC_TIMESTAMP" to System.currentTimeMillis())
    return Result.success(outputData)
}

你可能会疑问:为什么没有在后续任务中获取此次任务的执行结果的演示?因为这涉及到了 WorkManager 的链式任务。

链式任务

假设我们定义了三个后台任务,分别是同步数据、压缩数据和上传数据。我们想要先同步数据、然后压缩数据、最后上传数据。这时,就可以使用链式任务来完成。

kotlin 复制代码
// 假设已定义好 SyncWorker, CompressWorker, UploadWorker
val syncWork = OneTimeWorkRequest.Builder(SyncWorker::class.java).build()
val compressWork = OneTimeWorkRequest.Builder(CompressWorker::class.java).build()
val uploadWork = OneTimeWorkRequest.Builder(UploadWorker::class.java).build()

WorkManager.getInstance(applicationContext)
    // 以 syncWork 开始
    .beginWith(syncWork)
    // 然后执行 compressWork
    .then(compressWork)
    // 最后执行 uploadWork
    .then(uploadWork)
    .enqueue()

我们使用了 beginWith() 方法开启链式任务,使用 then() 方法连接了后续的任务。

在这个链条中,前一个任务成功后返回的 outputData,会自动作为下一个任务的 inputData,这样就实现了任务间的结果传递。

如果链条中的任何一个任务运行失败(返回 Result.failure())或被取消了,其后续的所有任务都不会被执行。

失败重试策略

前面我们说过,当 doWork() 方法返回 Result.retry() 时,WorkManager 会根据我们设置的退避策略来进行重试,这对于处理网络请求不稳定的场景来说非常有用。

我们可以通过 setBackoffCriteria() 方法来设置退避策略。例如:

kotlin 复制代码
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,  // 退避策略
        10, // 首次重试等待时长
        TimeUnit.SECONDS // 时间单位
    )
    .build()

我们当前的首次重试等待时长为 10 秒,注意重试时长不能短于 10 秒。

退避策略有两种可选,分别是 LINEAREXPONENTIALLINEAR 表示下次重试的等待时间会以线性方式增加,也就是固定增加;EXPONENTIAL 表示下次重试的等待时间会以指数的方式增加。

为什么要指定重试等待时间的增长策略?

因为如果任务一直失败,此时,不断地重复是没有意义的。我们让重试的时间随着失败次数的增多而增加,这才更加合理。

观察任务状态

我们可以通过 LiveData 对象来观察一个 WorkRequest 任务请求的状态变化。

只需调用 getWorkInfoByIdLiveData() 方法即可获取一个 LiveData 对象,例如:

kotlin 复制代码
WorkManager.getInstance(applicationContext)
    .getWorkInfoByIdLiveData(request.id) // 任务请求的 id
    .observe(this) { workInfo ->
        workInfo?.let { info ->
            when (info.state) {
                WorkInfo.State.SUCCEEDED -> {
                    Log.d("MainActivity", "任务成功!")
                    // 可以通过 workInfo.outputData 获取输出数据
                    val timestamp = workInfo.outputData.getLong("SYNC_TIMESTAMP", 0)
                }
                WorkInfo.State.FAILED -> Log.d("MainActivity", "任务失败!")
                WorkInfo.State.RUNNING -> Log.d("MainActivity", "任务正在运行...")
                else -> { /* 其他状态如 ENQUEUED, CANCELLED, BLOCKED */ }
            }
        }

    }

你也可以调用 getWorkInfosByTagLiveData() 方法,来获取同一标签名下所有任务请求的 LiveData 对象。

最后,在真实项目中,我们会将 WorkManager 的相关操作(创建任务请求、入队)的逻辑封装在 Repository 层,由 ViewModel 调用,而不是直接写在 Activity 或 Fragment 中。

另外,由于国内手机厂商对 Android 系统的高度定制(如"一键杀死应用"的后台管理策略),WorkManager 的任务调度可能在某些国产手机上无法正常工作,导致严重延迟甚至不执行。所以你绝对不要依赖 WorkManager 实现需要精准实时执行的核心业务功能。 但对于像数据同步、日志上传这类任务还是不错的选择。

相关推荐
没有了遇见36 分钟前
Android 渐变色实现总结
android
mmoyula5 小时前
【RK3568 驱动开发:实现一个最基础的网络设备】
android·linux·驱动开发
sam.li6 小时前
WebView安全实现(一)
android·安全·webview
移动开发者1号7 小时前
Kotlin协程超时控制:深入理解withTimeout与withTimeoutOrNull
android·kotlin
程序员JerrySUN7 小时前
RK3588 Android SDK 实战全解析 —— 架构、原理与开发关键点
android·架构
移动开发者1号7 小时前
Java Phaser:分阶段任务控制的终极武器
android·kotlin
哲科软件16 小时前
跨平台开发的抉择:Flutter vs 原生安卓(Kotlin)的优劣对比与选型建议
android·flutter·kotlin
jyan_敬言1 天前
【C++】string类(二)相关接口介绍及其使用
android·开发语言·c++·青少年编程·visual studio
程序员老刘1 天前
Android 16开发者全解读
android·flutter·客户端