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,你只需要做两件事:

  1. 继承CoroutineWorker而不是Worker
  2. 重写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()现在是一个挂起函数,你可以在里面自由使用withContextdelay、或者调用其他挂起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的核心知识点:

CoroutineWorker 继承 CoroutineWorker 重写 suspend doWork() 返回 Result 协程作用域管理 自动取消支持 进度上报 前台任务支持 避免 GlobalScope 注意超时限制 不要阻塞协程 CoroutineWorker 知识体系:继承方式、核心特性、注意事项

总结

协程Worker说白了就是让后台任务写起来更像普通协程代码。你不需要关心线程池、不需要手动管理生命周期、不需要处理复杂的取消逻辑。WorkManager帮你兜底了。

我个人建议,新项目一律使用CoroutineWorker,老项目迁移也很简单——把Worker改成CoroutineWorkerdoWork()加上suspend关键字,基本就完事了。唯一要注意的就是上面提到的几个坑,尤其是CancellationException的处理。

好了,这一章就到这里。代码写起来,坑踩起来,经验积累起来,你也能写出健壮的后台任务。


公众号:蓝海资料掘金营,微信deep3321