11、WorkManager KTX:WorkManager的协程Worker,简化后台任务
后台任务,一直是Android开发里绕不开的坑。我记得早期用Service加线程池做后台同步,一不小心就碰上进程被杀的惨案。后来有了WorkManager,算是官方给了一套靠谱的解决方案。但说实话,WorkManager刚出来那会儿,用起来还是有点啰嗦——继承Worker,重写doWork,返回Result,每一步都得写模板代码。
直到KTX扩展出现,配合Kotlin协程,才真正让后台任务写起来像在写普通函数。嗯,今天我们就聊聊这个。
为什么需要协程Worker?
先看一个原生Worker的例子:
class UploadWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
return try {
// 这里不能直接调用挂起函数
val data = uploadSync() // 同步阻塞调用
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
你发现问题了吗?doWork()是同步方法,里面不能直接调用协程挂起函数。如果你非要用协程,就得自己包一层runBlocking。我在项目中第一次这么干的时候,就被同事提醒了——runBlocking在后台线程上跑,虽然能工作,但会阻塞线程,浪费资源。
说白了,原生Worker和协程世界是割裂的。而CoroutineWorker就是来填这个坑的。
CoroutineWorker:协程原生的Worker
使用CoroutineWorker,你只需要做两件事:
- 继承
CoroutineWorker而不是Worker - 重写
doWork()为挂起函数
看代码:
class UploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 这里可以直接调用挂起函数
val result = uploadAsync()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
是不是清爽多了?doWork()现在是一个挂起函数,你可以在里面自由使用withContext、delay、或者调用其他挂起API。协程调度完全由WorkManager管理,你不需要关心线程切换。
核心区别:
- Worker.doWork() → 同步阻塞,返回Result
- CoroutineWorker.doWork() → 挂起函数,返回Result
- CoroutineWorker默认运行在Dispatchers.Default上
协程作用域与生命周期
这里有个细节我特别想强调。原生Worker的doWork()一旦返回,Worker就结束了。但协程Worker不一样——它内部有一个coroutineContext,WorkManager会在任务被取消时自动取消协程。
什么意思呢?
假设你的后台任务正在上传一个大文件,用户突然退出了App。WorkManager会收到系统信号,然后取消对应的协程。你的挂起函数会抛出一个CancellationException,你可以捕获它做清理工作。
override suspend fun doWork(): Result {
return try {
withContext(Dispatchers.IO) {
// 模拟耗时上传
for (i in 0..100) {
// 检查是否被取消
ensureActive()
uploadChunk(i)
}
}
Result.success()
} catch (e: CancellationException) {
// 做清理工作
cleanup()
throw e // 必须重新抛出,否则WorkManager不知道任务被取消
} catch (e: Exception) {
Result.retry()
}
}
我曾经踩过一个坑:在catch块里捕获了CancellationException但没有重新抛出,结果WorkManager以为任务正常完成了,实际上数据根本没上传完。嗯,这一点要记住——CancellationException必须重新抛出,或者直接用try/finally做清理。
setForeground与协程
对于需要长时间运行的后台任务(比如下载大文件),你可能需要显示一个通知。原生Worker用setForegroundAsync(),而协程Worker提供了更简洁的方式:
class DownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 创建通知
val notification = createNotification("下载中...")
// 设置为前台任务
setForeground(notification)
return try {
downloadFile()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
注意,setForeground()必须在doWork()开始后尽快调用,而且必须在挂起之前。我习惯在doWork()的第一行就调用它。
结合Flow与LiveData
协程Worker还有一个隐藏技能——它可以和Flow配合,实现进度上报。你想想看,以前要上报下载进度,得用setProgress()配合LiveData观察。现在有了协程,我们可以用flow更优雅地处理:
class ProgressWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val total = 100
for (i in 0..total) {
// 更新进度
setProgress(
workDataOf(
"progress" to i,
"total" to total
)
)
delay(100) // 模拟耗时
}
return Result.success()
}
}
// 在Activity或Fragment中观察
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(workId)
.observe(viewLifecycleOwner) { workInfo ->
if (workInfo.state == WorkInfo.State.RUNNING) {
val progress = workInfo.progress.getInt("progress", 0)
// 更新UI
}
}
这里有个小技巧:setProgress()是线程安全的,你可以在协程的任何地方调用它。但要注意,频繁调用会带来性能开销,我一般每10%或每1秒更新一次。
避坑指南:协程Worker的常见问题
我在项目中用协程Worker也遇到过几个坑,分享给你:
坑1:不要用GlobalScope
有些新手会在doWork()里启动GlobalScope.launch,这会导致协程脱离WorkManager的生命周期管理。任务被取消时,这些协程还在跑。正确做法是直接用doWork()的挂起函数,或者用coroutineScope创建子作用域。
坑2:注意超时设置
WorkManager默认有10分钟的超时限制。如果你的协程任务超过10分钟,会被强制取消。对于长时间任务,记得设置setForeground()或者调整超时时间:
val request = OneTimeWorkRequestBuilder<UploadWorker>()
.setInitialDelay(1, TimeUnit.MINUTES)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
坑3:不要阻塞协程
在doWork()里调用Thread.sleep()或者runBlocking,会浪费线程资源。用delay()代替sleep(),用挂起函数代替阻塞调用。
知识体系图
下面这张图总结了协程Worker的核心知识点:
总结
协程Worker说白了就是让后台任务写起来更像普通协程代码。你不需要关心线程池、不需要手动管理生命周期、不需要处理复杂的取消逻辑。WorkManager帮你兜底了。
我个人建议,新项目一律使用CoroutineWorker,老项目迁移也很简单——把Worker改成CoroutineWorker,doWork()加上suspend关键字,基本就完事了。唯一要注意的就是上面提到的几个坑,尤其是CancellationException的处理。
好了,这一章就到这里。代码写起来,坑踩起来,经验积累起来,你也能写出健壮的后台任务。