请求取消与生命周期管理:协程的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,结果就是空指针或者内存泄漏。
正确的做法是:
- 用lifecycleScope或viewModelScope,让框架帮你管理取消。
- 如果必须用自定义Scope,记得在onDestroy里取消。
- 不要在协程里直接持有Activity/Fragment的引用,用LiveData或StateFlow传递数据。
避坑指南:我曾经在项目里用 lifecycleScope 启动了一个协程,里面调用了 withContext(Dispatchers.IO)。结果发现,即使Activity销毁了,IO线程池里的任务还在跑。后来才明白,withContext 只是切换线程,取消信号需要协程自己响应。如果IO操作不支持中断,那就真的取消不了。所以,尽量用支持取消的库,比如OkHttp的请求。
SVG:协程取消与生命周期管理流程图
总结
协程的取消机制,说白了就是一套“优雅退出”的协议。你不需要暴力中断线程,只需要告诉协程“该停了”,它会在合适的时机自己停下来。
在Android开发中,lifecycleScope 和 viewModelScope 是我们最得力的助手。它们帮我们自动处理了生命周期和取消的逻辑,让我们能专注于业务代码。
嗯,最后再啰嗦一句:不要用GlobalScope。除非你真的知道自己在做什么,否则它带来的麻烦远大于便利。