17、协程与WorkManager:WorkManager的协程支持、CoroutineWorker的使用、长时运行任务的协程管理、WorkManager与协程的取消策略
说实话,WorkManager 刚出来那会儿,我其实有点抵触。总觉得 Google 又在造轮子。直到我在一个项目里要处理「上传日志 + 同步配置 + 清理缓存」这种后台任务链,才发现自己以前用 Service + 定时器那套方案有多脆弱——手机一杀进程,全白干。
后来 WorkManager 加入了协程支持,嗯,这才对味了。它把生命周期感知、任务持久化、电量优化这些脏活累活都包了,我们只需要写业务逻辑。今天我们就来聊聊,协程和 WorkManager 怎么配合,才能写出既优雅又健壮的后台任务。
WorkManager 的协程支持:从 ListenableWorker 到 CoroutineWorker
WorkManager 本身是基于 ListenableWorker 的,它用 ListenableFuture 做异步回调。说实话,这玩意儿写起来挺别扭的——你要么用 Guava 的 Futures,要么自己转成回调。我早期项目里就吃过这个亏,回调嵌套多了,代码跟意大利面似的。
后来 Google 推出了 CoroutineWorker,本质上是把 ListenableWorker 的异步模型用协程包装了一层。你想想看,我们只需要继承 CoroutineWorker,重写 suspend doWork() 方法,就能直接用协程了。多舒服。
核心区别:
ListenableWorker:返回ListenableFuture<Result>,需要手动管理线程CoroutineWorker:返回suspend Result,自动运行在Dispatchers.Default上
CoroutineWorker 的使用:一个完整的例子
直接上代码吧。假设我们要做一个「夜间数据同步」的任务——把本地缓存的用户操作记录上传到服务器。
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 注意:这里默认运行在 Dispatchers.Default
return try {
val records = fetchLocalRecords()
uploadToServer(records)
clearLocalRecords()
Result.success()
} catch (e: IOException) {
// 网络错误,可以重试
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
} catch (e: Exception) {
// 其他错误,直接失败
Result.failure()
}
}
private suspend fun fetchLocalRecords(): List<Record> {
// 用 Room 查询,Room 本身支持协程
return withContext(Dispatchers.IO) {
database.recordDao().getUnsyncedRecords()
}
}
private suspend fun uploadToServer(records: List<Record>) {
// 网络请求,用 Retrofit 的协程支持
apiService.uploadRecords(records)
}
}
这里有个细节我特别想提一下:doWork() 是 suspend 函数,但它的调度器是 Dispatchers.Default。如果你在里面做数据库操作或网络请求,最好用 withContext(Dispatchers.IO) 切一下。我曾经见过有人直接在 doWork() 里用 GlobalScope.launch 开新协程——千万别这么干,WorkManager 不会等你那个协程结束的。
长时运行任务的协程管理
有些任务不是几秒钟能搞定的。比如下载一个大文件、处理一批图片。这时候用普通的 CoroutineWorker 可能会被系统杀掉——因为 WorkManager 默认认为你的任务应该在 10 分钟内完成。
对于长时任务,WorkManager 提供了 ForegroundInfo 机制。说白了,就是让你的任务变成一个前台服务,显示一个通知。这样系统就不会轻易杀你了。
class DownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 设置前台通知
val notification = createNotification("正在下载...")
setForeground(ForegroundInfo(1, notification))
return try {
downloadLargeFile()
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(1, createNotification("下载中"))
}
private fun createNotification(content: String): Notification {
// 构建通知,省略具体代码
}
}
我个人习惯在 doWork() 一开始就调用 setForeground(),而不是等到真正开始下载才设置。为什么?因为从任务被调度到真正执行,中间可能有几秒延迟。如果用户在这几秒内把应用划掉,你的任务可能还没拿到前台权限就被杀了。嗯,这个坑我踩过。
小技巧:如果你需要定期更新通知进度,可以在协程里用 withContext(NonCancellable) 来更新通知。因为 setForeground() 本身不是挂起函数,但更新通知可能需要切到主线程。
WorkManager 与协程的取消策略
这是最容易出问题的地方。WorkManager 有自己的取消机制——当你调用 WorkManager.cancelWorkById() 时,它会尝试中断你的 Worker。对于 CoroutineWorker,这个中断是通过协程的取消协作机制实现的。
说白了,WorkManager 会取消 doWork() 所在的协程。如果你的代码没有正确处理取消信号,任务可能不会真正停止。
class CancellableWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 模拟一个可取消的循环
for (i in 0 until 100) {
// 检查是否被取消
if (isStopped) {
return Result.failure()
}
// 或者用 ensureActive()
ensureActive()
processItem(i)
}
Result.success()
} catch (e: CancellationException) {
// 协程被取消时,这里会被触发
// 注意:不要吞掉 CancellationException
throw e
}
}
private suspend fun processItem(index: Int) {
// 模拟耗时操作
delay(100)
}
}
这里有个关键点:isStopped 是 CoroutineWorker 提供的属性,它会在 WorkManager 要求停止时变为 true。而 ensureActive() 是协程的取消检查函数。我个人建议两者都用——isStopped 用于快速返回,ensureActive() 用于抛出 CancellationException 让协程框架处理。
我曾经踩过的坑:在 doWork() 里用了 try-catch 捕获了 CancellationException 但没有重新抛出。结果任务被取消后,WorkManager 以为它正常完成了,返回了 Result.success()。这会导致下次调度时,系统认为这个任务已经成功执行了,不会再重试。正确的做法是:要么不捕获 CancellationException,要么捕获后重新抛出。
知识体系总览
下面这张图梳理了协程与 WorkManager 的核心关系。你可以看到,从任务定义到执行调度,再到取消处理,整个链路都是围绕协程的挂起和取消机制展开的。
一些实战建议
最后,分享几个我在实际项目中积累的经验:
- 不要用 GlobalScope:在
CoroutineWorker里开GlobalScope.launch会导致协程生命周期与 Worker 脱节。任务取消后,那个协程还在跑,浪费资源。 - 合理设置重试策略:
Result.retry()配合BackoffPolicy使用。我一般设置指数退避,初始延迟 10 秒,最多重试 3 次。网络波动时很管用。 - 测试取消逻辑:在单元测试里模拟
WorkManager.cancelWorkById(),验证你的 Worker 是否能正确响应取消。我见过太多人只测正常流程,结果线上取消时出问题。 - 注意内存泄漏:如果
doWork()里持有 Activity 或 Fragment 的引用,即使任务被取消,也可能导致内存泄漏。用applicationContext或者只传必要的数据。
好了,关于协程与 WorkManager 的配合,核心就是这些。说白了,就是把 WorkManager 的生命周期管理能力和协程的异步能力结合起来。你只要记住:用 CoroutineWorker 替代 ListenableWorker,用 isStopped 和 ensureActive() 处理取消,用 ForegroundInfo 处理长时任务。剩下的,交给协程的挂起和恢复机制就好。
一句话总结:WorkManager 负责「什么时候做、做多久、要不要重做」,协程负责「怎么做、怎么停、怎么不卡主线程」。两者各司其职,配合起来非常舒服。
公众号:蓝海资料掘金营,微信deep3321