前言
很早之前,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
定义了要执行哪个任务以及它该如何运行。它有两个子类,分别是 OneTimeWorkRequest
和 PeriodicWorkRequest
。
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()
将任务请求加入队列
通过 WorkManager
的 enqueue()
方法将 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" />
在 MainActivity
的 onCreate()
方法中添加如下内容:
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 秒。
退避策略有两种可选,分别是 LINEAR
和 EXPONENTIAL
。LINEAR
表示下次重试的等待时间会以线性方式增加,也就是固定增加;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 实现需要精准实时执行的核心业务功能。 但对于像数据同步、日志上传这类任务还是不错的选择。