请求取消与生命周期管理:协程的Job取消、lifecycleScope自动取消、避免内存泄漏

网络请求的取消,说实话,在很多项目里是被忽视的。我见过不少App,页面都销毁了,网络请求还在后台跑着。结果呢?回调回来时,UI已经没了,轻则报个空指针,重则直接内存泄漏。

协程的出现,让这件事变得优雅多了。今天我们就来聊聊,怎么用协程把请求取消这件事做好。

协程取消的核心:Job

每个协程都有一个Job对象。你可以把它理解成协程的“身份证”加“遥控器”。

取消协程,本质上就是调用 job.cancel()。但这里有个坑——不是所有协程都会乖乖听话。

关键点:协程的取消是协作式的。意思是,协程代码本身需要配合取消,否则你调了cancel也没用。

举个例子:

val job = scope.launch {
    // 模拟耗时操作
    for (i in 0..1000) {
        // 这里没有检查取消状态
        delay(10) // delay 本身会响应取消
    }
}

// 取消
job.cancel()
job.join() // 等待取消完成

delay()yield() 这些挂起函数,内部会检查协程是否被取消。但如果你写的是纯计算代码,比如一个大循环,那就得自己检查了。

scope.launch {
    for (i in 0..1000000) {
        // 手动检查取消状态
        if (!isActive) return@launch
        // 执行计算...
    }
}

我个人习惯用 ensureActive(),它会在协程取消时直接抛出 CancellationException

scope.launch {
    for (i in 0..1000000) {
        ensureActive()
        // 执行计算...
    }
}

小技巧:在Retrofit的网络请求中,如果你用 withContext(Dispatchers.IO) 包裹了挂起函数,取消会自动传播到OkHttp的请求上。也就是说,协程取消了,底层的HTTP请求也会被取消。这一点非常省心。

lifecycleScope:Android的救星

在Android开发中,最头疼的就是生命周期管理。Activity和Fragment有那么多状态,稍不注意就出问题。

Jetpack的 lifecycleScope 就是专门解决这个问题的。它是 LifecycleOwner 的一个扩展属性,会在Lifecycle销毁时自动取消协程。

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 这个协程会在Activity销毁时自动取消
        lifecycleScope.launch {
            val result = apiService.fetchData()
            updateUI(result)
        }
    }
}

你看,不需要手动管理任何取消逻辑。Activity销毁了,协程自动取消,UI更新也不会执行了。

但这里有个细节——lifecycleScope 默认是在 Lifecycle.State.STARTED 之后才启动协程。如果你需要在 CREATED 状态就启动,可以用 lifecycleScope.launchWhenCreated

作用域 启动时机 取消时机
lifecycleScope.launch STARTED DESTROYED
launchWhenCreated CREATED DESTROYED
launchWhenResumed RESUMED DESTROYED

注意:我曾经在项目里遇到过一个问题——用 lifecycleScope.launchWhenStarted 启动了一个网络请求,但用户按Home键后,Activity进入STOPPED状态,协程被暂停了。等用户回来时,请求才继续。这其实不是取消,是暂停。如果你希望Activity不可见时就取消请求,得用 lifecycleScope.launch 配合 repeatOnLifecycle

viewModelScope:ViewModel的专属作用域

除了 lifecycleScope,还有一个 viewModelScope。它会在ViewModel的 onCleared() 时自动取消。

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val data = repository.fetchData()
            _uiState.value = UiState.Success(data)
        }
    }
}

ViewModel的生命周期比Activity长,特别是屏幕旋转时,ViewModel不会被销毁。所以网络请求在旋转屏幕时不会中断,用户体验更好。

我个人建议:网络请求尽量放在ViewModel里,用viewModelScope管理。Activity和Fragment只负责展示数据。

避免内存泄漏:实战经验

内存泄漏,说白了就是对象被不该持有的人一直拽着不放。协程如果持有Activity的引用,Activity销毁了但协程还在跑,那就泄漏了。

我遇到过最典型的场景是这样的:

// 错误示例
class MyActivity : AppCompatActivity() {
    private lateinit var job: Job
    
    fun startRequest() {
        job = GlobalScope.launch {
            val result = apiService.fetchData()
            // 这里隐式持有Activity的引用
            textView.text = result
        }
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // 忘了取消job
    }
}

这个例子用了 GlobalScope,它不会自动取消。Activity销毁了,协程还在跑,还试图更新 textView,结果就是空指针或者内存泄漏。

正确的做法是:

  1. 用lifecycleScope或viewModelScope,让框架帮你管理取消。
  2. 如果必须用自定义Scope,记得在onDestroy里取消。
  3. 不要在协程里直接持有Activity/Fragment的引用,用LiveData或StateFlow传递数据。

避坑指南:我曾经在项目里用 lifecycleScope 启动了一个协程,里面调用了 withContext(Dispatchers.IO)。结果发现,即使Activity销毁了,IO线程池里的任务还在跑。后来才明白,withContext 只是切换线程,取消信号需要协程自己响应。如果IO操作不支持中断,那就真的取消不了。所以,尽量用支持取消的库,比如OkHttp的请求。

SVG:协程取消与生命周期管理流程图

协程取消与生命周期管理 Job 取消 job.cancel() 协作式取消 检查 isActive ensureActive() lifecycleScope Activity/Fragment Lifecycle 销毁 自动取消协程 避免内存泄漏 viewModelScope ViewModel onCleared() 自动取消协程 屏幕旋转不中断 核心原则:谁启动,谁管理,谁取消

总结

协程的取消机制,说白了就是一套“优雅退出”的协议。你不需要暴力中断线程,只需要告诉协程“该停了”,它会在合适的时机自己停下来。

在Android开发中,lifecycleScopeviewModelScope 是我们最得力的助手。它们帮我们自动处理了生命周期和取消的逻辑,让我们能专注于业务代码。

嗯,最后再啰嗦一句:不要用GlobalScope。除非你真的知道自己在做什么,否则它带来的麻烦远大于便利。