Worker生命周期与线程:后台任务的核心机制

说实话,Worker的生命周期和线程模型,是很多Android开发者容易忽略但又极其重要的部分。我记得刚接触WorkManager那会儿,以为它就是个简单的任务队列,直到线上出现了一个诡异的ANR——一个Worker跑了太久,把主线程给卡住了。嗯,从那以后,我对Worker的线程模型就格外上心。

Worker运行在后台线程

先明确一点:Worker默认运行在后台线程。WorkManager内部维护了一个线程池,你的doWork()方法就在这个线程池里执行。这意味着你不需要手动开线程,也不需要担心阻塞主线程。

核心要点:Worker的doWork()方法运行在WorkManager的默认后台线程池中,线程优先级为THREAD_PRIORITY_BACKGROUND。

但这里有个坑——Worker本身不是线程安全的。如果你在Worker里访问了共享的可变状态,比如一个静态变量或者单例对象,那就要小心了。我遇到过一个问题:两个Worker同时修改同一个静态Map,结果数据全乱了。解决方案很简单:要么加锁,要么用不可变数据结构。

class MyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

    override fun doWork(): Result {
        // 这里已经在后台线程了
        // 可以放心做耗时操作
        val result = fetchDataFromNetwork()
        saveToDatabase(result)
        return Result.success()
    }
}

你可能会问:如果doWork()里抛异常了怎么办?Worker会捕获异常,然后返回Result.failure()。但注意,未捕获的异常会导致Worker被销毁,任务状态变成失败。所以,我习惯在doWork()里加个try-catch,把异常信息记录下来,方便排查问题。

注意:不要在doWork()里调用Thread.sleep()或者无限循环。Worker有10分钟的超时限制(Android 12+),超时后会被强制停止,返回Result.failure()。

CoroutineWorker:协程时代的首选

如果你项目里用了Kotlin协程,那CoroutineWorker就是你的最佳选择。它比普通Worker更轻量,而且天然支持协程的取消和结构化并发。

我个人的习惯是:新项目一律用CoroutineWorker。为什么?因为协程的挂起函数可以让你写出更简洁的异步代码,而且不需要手动管理线程池。

class MyCoroutineWorker(context: Context, params: WorkerParameters) : 
    CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        // 这里已经是协程上下文了
        // 可以调用任何挂起函数
        val data = fetchDataFromApi() // 挂起函数
        processData(data) // 普通函数
        return Result.success()
    }
}

CoroutineWorker的doWork()是一个挂起函数,它运行在Dispatchers.Default上。这意味着你可以直接调用其他挂起函数,而不需要手动切换线程。而且,当Worker被取消时,协程会自动取消,不会留下僵尸任务。

小技巧:如果你需要在CoroutineWorker里切换线程,可以用withContext(Dispatchers.IO)来做IO操作。但记住,不要用Dispatchers.Main,因为Worker本身就不该碰UI。

我记得有一次,一个同事用CoroutineWorker做图片上传,结果发现上传进度回调没反应。排查了半天,发现他在doWork()里用了GlobalScope.launch。嗯,这是个经典错误——在CoroutineWorker里不要用GlobalScope,应该用this(即CoroutineScope)来启动协程,这样才能被正确取消。

RxWorker:响应式编程的集成方案

如果你的项目重度使用RxJava,那RxWorker就是为你准备的。它把Worker的生命周期和RxJava的Observable/Flowable绑定在一起,支持背压和操作符链。

class MyRxWorker(context: Context, params: WorkerParameters) : 
    RxWorker(context, params) {

    override fun createWork(): Single<Result> {
        return fetchDataFromApi()
            .map { data -> processData(data) }
            .map { Result.success() }
            .onErrorReturn { Result.failure() }
    }
}

RxWorker的createWork()方法返回一个Single<Result>。当这个Single被订阅时,Worker开始执行;当Single完成或出错时,Worker结束。而且,RxWorker会自动处理取消——如果Worker被取消,Single的订阅会被自动销毁。

不过说实话,现在新项目用RxJava的越来越少了。我个人建议:除非你维护的是老项目,否则优先考虑CoroutineWorker。RxWorker的优点是操作符丰富,但缺点是学习曲线陡峭,而且容易写出内存泄漏的代码。

对比总结:

类型 线程模型 取消支持 适用场景
Worker 默认后台线程池 手动检查isStopped 简单任务,不需要协程/Rx
CoroutineWorker Dispatchers.Default 自动取消协程 新项目,协程项目
RxWorker RxJava调度器 自动取消订阅 老项目,RxJava项目

自定义线程池与Worker

有时候,默认的线程池满足不了你的需求。比如,你想限制并发数,或者给任务设置优先级。这时候就需要自定义线程池了。

WorkManager允许你通过Configuration来设置自定义的Executor。我曾在项目中遇到过一个问题:多个Worker同时下载大文件,导致内存飙升。后来我自定义了一个线程池,把最大并发数限制在3个,问题就解决了。

// 自定义线程池
val customExecutor = ThreadPoolExecutor(
    2, // 核心线程数
    4, // 最大线程数
    30, TimeUnit.SECONDS, // 空闲线程存活时间
    LinkedBlockingQueue<Runnable>(), // 任务队列
    ThreadFactory { runnable ->
        Thread(runnable, "work-manager-custom-thread").apply {
            priority = Process.THREAD_PRIORITY_BACKGROUND
        }
    }
)

// 初始化WorkManager时传入
val config = Configuration.Builder()
    .setExecutor(customExecutor)
    .build()
WorkManager.initialize(context, config)

这里要注意:自定义线程池会影响所有Worker,不只是你当前的这个。所以,如果你只想给某个特定Worker设置线程池,那就不太合适了。更好的做法是:在Worker内部自己管理线程。

比如,在CoroutineWorker里,你可以用withContext切换到自定义的Dispatcher:

class CustomDispatcherWorker(context: Context, params: WorkerParameters) : 
    CoroutineWorker(context, params) {

    private val customDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()

    override suspend fun doWork(): Result {
        return withContext(customDispatcher) {
            // 在自定义线程池中执行
            doHeavyWork()
            Result.success()
        }
    }

    override fun onStopped() {
        super.onStopped()
        // 记得关闭自定义Dispatcher
        customDispatcher.close()
    }
}

警告:自定义线程池一定要记得关闭,否则会导致内存泄漏。在Worker的onStopped()或finalize()里关闭是个好习惯。

我曾经犯过一个错误:在Worker里创建了一个线程池,但忘了关闭。结果每次执行Worker都新建一个线程池,最后线程数暴涨,应用直接OOM了。嗯,从那以后,我每次创建线程池都会在注释里写上「记得关闭」。

Worker生命周期图解

下面这张图展示了Worker从创建到销毁的完整生命周期,以及线程模型的关系:

Worker生命周期与线程模型 ENQUEUED RUNNING SUCCEEDED FAILED CANCELLED 线程模型说明 • Worker:运行在 WorkManager 默认后台线程池(ThreadPoolExecutor) • CoroutineWorker:运行在 Dispatchers.Default,支持协程取消 • RxWorker:运行在 RxJava 调度器,支持背压和操作符链 • 自定义线程池:通过 Configuration.setExecutor() 全局替换,或在 Worker 内部管理 生命周期回调:onStopped() 在 CANCELLED 或 RUNNING 超时时触发

从这张图可以看出,Worker的生命周期其实很简单:入队→运行→结束(成功/失败/取消)。但线程模型的选择会影响你在每个阶段能做什么。比如,在RUNNING阶段,普通Worker需要手动检查isStopped,而CoroutineWorker和RxWorker会自动处理取消。

最后,我想说一句:没有银弹。选择哪种Worker,取决于你的项目现状和团队技术栈。但不管选哪种,理解它的线程模型和生命周期,是写出稳定后台任务的前提。嗯,希望这些经验能帮你少踩几个坑。