Retrofit 请求取消与生命周期绑定:从 Call.cancel 到协程可取消设计
为什么需要取消请求
用户退出页面、切换 Tab、网络超时------这些场景下,正在进行的网络请求如果不取消,会带来三个问题:
- 资源浪费:后台继续占用网络、线程、内存
- 状态错乱:回调触发时页面已销毁,导致空指针或状态不一致
- 用户体验差:连续触发多个请求时,旧请求回调可能覆盖新请求结果
Retrofit 提供了完整的取消机制,从传统的 Call.cancel() 到协程的结构化并发,本文梳理这些能力的边界与落地方案。
Call.cancel() 基础
同步取消
kotlin
interface ApiService {
@GET("users/{id}")
fun getUser(@Path("id") id: String): Call<User>
}
val call = apiService.getUser("123")
call.enqueue(object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
// 处理响应
}
override fun onFailure(call: Call<User>, t: Throwable) {
if (call.isCanceled) {
// 主动取消,不弹错误提示
return
}
// 真实错误
}
})
// 页面销毁时取消
call.cancel()
机制:
cancel()会中断底层 OkHttp 的 Socket 读写- 回调的
onFailure会收到IOException,通过call.isCanceled区分主动取消和真实错误
多个 Call 统一管理
kotlin
class CallManager {
private val calls = mutableListOf<Call<*>>()
fun <T> track(call: Call<T>): Call<T> {
calls.add(call)
return call
}
fun cancelAll() {
calls.forEach { it.cancel() }
calls.clear()
}
}
// 在 ViewModel 或 Fragment 中
private val callManager = CallManager()
override fun onCleared() {
callManager.cancelAll()
}
协程中的自动取消
viewModelScope 自动绑定
kotlin
class UserViewModel : ViewModel() {
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = apiService.getUser(id) // suspend 函数
_userState.value = user
} catch (e: CancellationException) {
// 协程被取消,静默处理
} catch (e: Exception) {
// 真实错误
_error.value = e.message
}
}
}
}
自动绑定机制:
viewModelScope在ViewModel.onCleared()时自动取消所有子协程- Retrofit 的 suspend 函数底层使用
Call.enqueue+suspendCancellableCoroutine,支持协程取消
lifecycleScope 的坑
kotlin
// ❌ 错误:Fragment 销毁后协程继续执行
lifecycleScope.launch {
val data = apiService.getData()
// Fragment 可能已经 detach,updateUI 崩溃
updateUI(data)
}
// ✅ 正确:绑定 STARTED 状态
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
val data = apiService.getData()
updateUI(data)
}
}
边界:
lifecycleScope在DESTROYED时取消,但onDestroyView和DESTROYED之间有时间差repeatOnLifecycle(STARTED)保证页面不可见时挂起,可见时恢复
手动取消与超时控制
withTimeout 超时取消
kotlin
try {
withTimeout(5000) {
val user = apiService.getUser(id)
_state.value = user
}
} catch (e: TimeoutCancellationException) {
_error.value = "请求超时"
}
注意:
- OkHttp 本身有连接超时、读写超时配置
withTimeout是协程层超时,两者叠加取较短值
Job 手动取消
kotlin
private var loadJob: Job? = null
fun loadData() {
loadJob?.cancel() // 取消旧请求
loadJob = viewModelScope.launch {
val data = apiService.getData()
_state.value = data
}
}
场景:搜索框输入防抖,连续输入时只保留最新请求。
生命周期安全的完整方案
方案一:ViewModel + StateFlow
kotlin
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
val state = _state.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_state.value = UiState.Loading
try {
val user = apiService.getUser(id)
_state.value = UiState.Success(user)
} catch (e: CancellationException) {
// 静默取消
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "未知错误")
}
}
}
}
// Fragment 中收集
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showData(state.data)
is UiState.Error -> showError(state.message)
}
}
}
}
优势:
- ViewModel 自动管理取消
- StateFlow 保证状态一致
repeatOnLifecycle避免后台更新 UI
方案二:封装可取消的 Repository
kotlin
class UserRepository {
suspend fun getUser(id: String): Result<User> = withContext(Dispatchers.IO) {
try {
val user = apiService.getUser(id)
Result.success(user)
} catch (e: CancellationException) {
throw e // 继续传播取消
} catch (e: Exception) {
Result.failure(e)
}
}
}
关键点:
CancellationException必须重新抛出,否则上层协程无法取消withContext可以切换线程,但不影响取消传播
多请求并发与取消
任一失败则全部取消
kotlin
suspend fun loadUserDetail(id: String) = coroutineScope {
try {
val user = async { apiService.getUser(id) }
val posts = async { apiService.getUserPosts(id) }
val followers = async { apiService.getFollowers(id) }
UserDetail(
user = user.await(),
posts = posts.await(),
followers = followers.await()
)
} catch (e: Exception) {
// 任一请求失败,coroutineScope 自动取消其他子协程
throw e
}
}
机制:
coroutineScope创建新作用域,子协程失败时自动取消兄弟协程supervisorScope则允许部分失败,其他继续执行
部分失败继续执行
kotlin
suspend fun loadUserDetailPartial(id: String) = supervisorScope {
val user = async { apiService.getUser(id) }
val posts = async { runCatching { apiService.getUserPosts(id) }.getOrNull() }
val followers = async { runCatching { apiService.getFollowers(id) }.getOrNull() }
UserDetail(
user = user.await(),
posts = posts.await(),
followers = followers.await()
)
}
实战踩坑
坑 1:取消后不清理状态
kotlin
// ❌ 取消后 Loading 状态残留
fun loadData() {
_state.value = UiState.Loading
viewModelScope.launch {
val data = apiService.getData()
_state.value = UiState.Success(data)
}
}
// ✅ 捕获取消,恢复 Idle
fun loadData() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val data = apiService.getData()
_state.value = UiState.Success(data)
} catch (e: CancellationException) {
_state.value = UiState.Idle
}
}
}
坑 2:在 finally 中执行挂起操作
kotlin
// ❌ finally 中执行挂起会抛异常
try {
val data = apiService.getData()
} finally {
saveToCache(data) // suspend 函数,抛 CancellationException
}
// ✅ 使用 NonCancellable
try {
val data = apiService.getData()
} finally {
withContext(NonCancellable) {
saveToCache(data)
}
}
坑 3:忘记传播取消
kotlin
// ❌ 吞掉取消异常
suspend fun loadData() {
try {
apiService.getData()
} catch (e: Exception) {
// CancellationException 也被捕获,上层无法取消
}
}
// ✅ 重新抛出取消
suspend fun loadData() {
try {
apiService.getData()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
// 处理其他异常
}
}
测试取消行为
kotlin
@Test
fun `请求取消时不更新状态`() = runTest {
val viewModel = UserViewModel(fakeRepository)
val job = launch {
viewModel.state.collect {
// 收集状态变化
}
}
viewModel.loadUser("123")
advanceTimeBy(100) // 模拟请求中
job.cancel() // 取消收集
advanceUntilIdle()
// 验证取消后状态未更新
assertEquals(UiState.Loading, viewModel.state.value)
}
小结
| 场景 | 方案 | 取消时机 |
|---|---|---|
| ViewModel | viewModelScope |
onCleared() |
| Fragment UI 更新 | lifecycleScope + repeatOnLifecycle(STARTED) |
页面不可见时 |
| 手动防抖 | Job.cancel() |
新请求触发时 |
| 超时控制 | withTimeout |
指定时间后 |
| 多请求失败联动 | coroutineScope |
任一子协程失败时 |
关键原则:
- 协程取消是协作式的,必须检查
isActive或调用挂起点 CancellationException必须重新抛出,不能吞掉- 在
finally中执行挂起操作需要NonCancellable
从 Call.cancel() 到协程的结构化并发,Retrofit 的取消机制既灵活又安全------只要遵守协程的取消契约,请求就能在合适的时机自动停止,不留后患。