15、WorkManager与协程集成:CoroutineWorker的doWork()挂起函数、协程取消与任务停止、withContext切换调度器、协程中的异常处理
说实话,WorkManager 刚出来那会儿,我还在用老一套的 Service + JobScheduler 组合拳。每次写后台任务都像在走钢丝——既要保证任务能跑完,又得小心别把用户手机搞卡了。直到 WorkManager 开始原生支持协程,我才觉得「嗯,这才是 Android 该有的样子」。
这一章,我们就来聊聊 WorkManager 和协程是怎么「联姻」的。说白了,就是 CoroutineWorker 这个关键角色。
15.1 CoroutineWorker 的 doWork() 挂起函数
传统的 Worker 类,它的 doWork() 是同步的。你必须在里面老老实实干活,不能挂起,不能切线程。但 CoroutineWorker 不一样——它的 doWork() 是一个 suspend 函数。
这意味着什么?意味着你可以在里面直接调用其他挂起函数,比如网络请求、数据库操作,完全不用手动管理线程。
class MyCoroutineWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 这里已经是协程环境了
val data = fetchDataFromNetwork() // 挂起函数
saveToDatabase(data) // 挂起函数
return Result.success()
}
}
我个人习惯把 doWork() 写得尽量简洁。复杂的逻辑拆成多个挂起函数,这样测试也方便。你想想看,如果所有代码都堆在 doWork() 里,那调试起来得多痛苦。
Dispatchers.Default 上。如果你的任务是 IO 密集型的,记得切到 Dispatchers.IO。
15.2 协程取消与任务停止
这里有个坑,我曾经踩过。WorkManager 在取消任务时,会调用 CoroutineWorker 的 onStopped() 方法,同时取消内部的协程。但如果你在 doWork() 里启动了新的协程作用域,那这些子协程可能不会被自动取消。
为什么会这样?因为 doWork() 本身是在一个协程里执行的,WorkManager 只会取消这个「根协程」。如果你用 GlobalScope 或者自定义的 CoroutineScope 启动了新协程,那它们就成了「野孩子」。
override suspend fun doWork(): Result {
// 正确做法:使用 coroutineScope 或 withContext
coroutineScope {
launch {
// 这个协程会被自动取消
doSubTask()
}
}
return Result.success()
}
我曾经在项目里遇到过一个问题:用户取消了任务,但后台还在偷偷跑着网络请求。排查了半天,发现是同事用 GlobalScope.launch 启动了子任务。嗯,从那以后我就在代码规范里加了一条——CoroutineWorker 里禁止使用 GlobalScope。
doWork() 会抛出一个 CancellationException。你需要确保自己的代码能优雅地处理这个异常,而不是直接崩溃。
15.3 withContext 切换调度器
刚才提到,CoroutineWorker 默认跑在 Dispatchers.Default 上。但实际开发中,我们经常需要切换调度器。比如网络请求用 Dispatchers.IO,UI 操作用 Dispatchers.Main(虽然 WorkManager 里不应该有 UI 操作)。
withContext 就是干这个的。它可以在不改变协程上下文的情况下,临时切换到另一个调度器执行代码块。
override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
// 这里跑在 IO 线程池
val result = performHeavyIoOperation()
// 切回 Default 或 Main 继续
Result.success(result)
}
}
我个人习惯把 withContext 用在那些「一小段代码需要切换线程」的场景。如果整个 doWork() 都是 IO 操作,那直接在类上指定调度器更省事:
class IoWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 所有代码都跑在 IO 上
}
// 重写这个属性来指定调度器
override val workDispatcher: CoroutineDispatcher
get() = Dispatchers.IO
}
你想想看,如果每个挂起函数都手动 withContext,代码会变得很啰嗦。所以,能用 workDispatcher 属性解决的,就别到处写 withContext。
15.4 协程中的异常处理
协程里的异常处理,和普通代码不太一样。在 CoroutineWorker 里,如果你不捕获异常,它会直接导致任务失败,返回 Result.failure()。
但有些异常是你预料之中的,比如网络超时。这时候你可能想重试,而不是直接失败。WorkManager 提供了 Result.retry() 来支持重试策略。
override suspend fun doWork(): Result {
return try {
val data = fetchDataFromNetwork()
saveToDatabase(data)
Result.success()
} catch (e: TimeoutException) {
// 网络超时,重试
Result.retry()
} catch (e: Exception) {
// 其他异常,记录日志并失败
Log.e("MyWorker", "任务失败", e)
Result.failure()
}
}
我曾经在项目里犯过一个错误:在 doWork() 里用了 try-catch 捕获所有异常,然后返回 Result.success()。结果任务明明失败了,WorkManager 却认为它成功了,导致数据不一致。嗯,这里要注意——不要吞掉异常。
另外,协程的 CoroutineExceptionHandler 在 CoroutineWorker 里不太适用。因为 doWork() 的异常会直接传递给 WorkManager,而不是通过协程的异常处理器。所以,老老实实用 try-catch 是最稳妥的。
- CoroutineWorker 的 doWork() 是挂起函数,天然支持协程
- 任务取消时,协程会被自动取消,但要注意子协程的作用域
- withContext 用于临时切换调度器,workDispatcher 属性用于全局指定
- 异常处理用 try-catch,不要吞异常,合理使用 Result.retry()
最后,我画了一张图,帮你理清 CoroutineWorker 的核心流程:
这张图把整个流程串起来了。从 WorkManager 调度开始,到 doWork() 执行,中间处理取消、切换调度器、异常捕获,最后返回结果。你写代码的时候,脑子里可以过一遍这个流程,不容易漏掉关键环节。