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 实现需要精准实时执行的核心业务功能。 但对于像数据同步、日志上传这类任务还是不错的选择。

相关推荐
松仔log2 小时前
Java中级——组合和继承
android·java·开发语言
我命由我123456 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian2337 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759627 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马7 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone7 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone7 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子11 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker11 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone11 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常