[Android 从零到一] Retrofit 请求取消与生命周期绑定:从 Call.cancel 到协程可取消设计

简介: 本文详解 Retrofit 请求取消与生命周期绑定:从传统 `Call.cancel()` 到协程结构化并发。涵盖资源浪费、状态错乱等取消必要性;对比手动管理、`viewModelScope`、`lifecycleScope + repeatOnLifecycle` 等方案;解析超时、多请求联动、常见踩坑及测试方法,强调取消契约与异常传播原则

Retrofit 请求取消与生命周期绑定:从 Call.cancel 到协程可取消设计

为什么需要取消请求

用户退出页面、切换 Tab、网络超时——这些场景下,正在进行的网络请求如果不取消,会带来三个问题:

  1. 资源浪费:后台继续占用网络、线程、内存
  2. 状态错乱:回调触发时页面已销毁,导致空指针或状态不一致
  3. 用户体验差:连续触发多个请求时,旧请求回调可能覆盖新请求结果

Retrofit 提供了完整的取消机制,从传统的 Call.cancel() 到协程的结构化并发,本文梳理这些能力的边界与落地方案。


Call.cancel() 基础

同步取消

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 统一管理

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 自动绑定

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
            }
        }
    }
}

自动绑定机制

  • viewModelScopeViewModel.onCleared() 时自动取消所有子协程
  • Retrofit 的 suspend 函数底层使用 Call.enqueue + suspendCancellableCoroutine,支持协程取消

lifecycleScope 的坑

// ❌ 错误: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)
    }
}

边界

  • lifecycleScopeDESTROYED 时取消,但 onDestroyViewDESTROYED 之间有时间差
  • repeatOnLifecycle(STARTED) 保证页面不可见时挂起,可见时恢复

手动取消与超时控制

withTimeout 超时取消

try {
    withTimeout(5000) {
        val user = apiService.getUser(id)
        _state.value = user
    }
} catch (e: TimeoutCancellationException) {
    _error.value = "请求超时"
}

注意

  • OkHttp 本身有连接超时、读写超时配置
  • withTimeout 是协程层超时,两者叠加取较短值

Job 手动取消

private var loadJob: Job? = null

fun loadData() {
    loadJob?.cancel() // 取消旧请求
    loadJob = viewModelScope.launch {
        val data = apiService.getData()
        _state.value = data
    }
}

场景:搜索框输入防抖,连续输入时只保留最新请求。


生命周期安全的完整方案

方案一:ViewModel + StateFlow

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

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 可以切换线程,但不影响取消传播

多请求并发与取消

任一失败则全部取消

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 则允许部分失败,其他继续执行

部分失败继续执行

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:取消后不清理状态

// ❌ 取消后 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 中执行挂起操作

// ❌ finally 中执行挂起会抛异常
try {
    val data = apiService.getData()
} finally {
    saveToCache(data) // suspend 函数,抛 CancellationException
}

// ✅ 使用 NonCancellable
try {
    val data = apiService.getData()
} finally {
    withContext(NonCancellable) {
        saveToCache(data)
    }
}

坑 3:忘记传播取消

// ❌ 吞掉取消异常
suspend fun loadData() {
    try {
        apiService.getData()
    } catch (e: Exception) {
        // CancellationException 也被捕获,上层无法取消
    }
}

// ✅ 重新抛出取消
suspend fun loadData() {
    try {
        apiService.getData()
    } catch (e: CancellationException) {
        throw e
    } catch (e: Exception) {
        // 处理其他异常
    }
}

测试取消行为

@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 任一子协程失败时

关键原则

  1. 协程取消是协作式的,必须检查 isActive 或调用挂起点
  2. CancellationException 必须重新抛出,不能吞掉
  3. finally 中执行挂起操作需要 NonCancellable

Call.cancel() 到协程的结构化并发,Retrofit 的取消机制既灵活又安全——只要遵守协程的取消契约,请求就能在合适的时机自动停止,不留后患。

相关文章
|
11天前
|
JSON Shell API
[鸿蒙从零到一] HarmonyOS 冷启动瀑布图分析与关键路径裁剪实战
本文详解HarmonyOS冷启动优化:先用HiTrace+DevEco Profiler获取可信瀑布图,精准识别“点击→首帧”关键路径;再通过延迟、并行、裁剪、预置四步法裁剪耗时,实测冷启动时间降低41.5%。强调“无图不优化”,拒绝盲目删初始化
64 1
|
12天前
|
自然语言处理 监控 Android开发
[Android 从零到一] APK 体积优化实战:从 50MB 到 15MB 的瘦身之旅
本文系统讲解APK瘦身完整方案:从APK Analyzer精准分析体积构成,到WebP图片压缩、资源混淆、R8代码优化、ABI过滤等实战技巧,结合动态模块与CI监控,助你轻松压缩APK体积50%以上,显著提升安装转化率
115 1
|
8天前
|
Java 测试技术 API
Android test全景:从单元 test到 UI 自动化的完整实践
本文系统讲解Android测试体系建设:涵盖JUnit/Truth单元测试、Mockito依赖隔离、Robolectric本地Android测试、Espresso UI测试及UI Automator跨应用测试,并集成JaCoCo覆盖率与GitHub Actions持续集成,助力打造高质量、可维护的Android应用。
70 0
|
11天前
|
缓存 监控 测试技术
Android 多渠道打包与 Gradle 构建优化实战
本文详解Android多渠道打包与Gradle构建优化实战,涵盖`productFlavors`、`buildTypes`、`flavorDimensions`核心配置,渠道信息注入(BuildConfig/Manifest/资源)、四大加速策略(缓存/并行/模块化/脚本优化),并提供从单构建到多维矩阵的演进案例,助你提升构建效率与工程化水平
90 0
|
13天前
|
消息中间件 存储 监控
Android 内存泄漏排查实战:从 LeakCanary 报警到根因定位
本文详解Android内存泄漏实战排查:基于LeakCanary报警快速定位HomeActivity等泄漏根因,覆盖Handler、单例Context、AsyncTask、LiveData、Bitmap五大高频场景,提供静态内部类+弱引用、lifecycleScope、ApplicationContext精准修复方案
72 0
|
13天前
|
网络协议 API 网络安全
HarmonyOS WebSocket 实战:断线重连、心跳保活与连接状态机设计
本文详解鸿蒙WebSocket高可用实践:直击弱网断连无感知、重连风暴、消息丢失三大痛点,提出“心跳保活+指数退避重连+显式状态机”三层架构,并提供完整ArkTS可运行代码,助开发者构建生产级实时通信能力。
63 0
|
18天前
|
缓存 大数据 数据库
Paging 3 分页加载:从配置到实战
Paging 3 是 Jetpack 高效分页库,支持自动分页、状态管理、网络+本地双缓存及 RecyclerView 无缝集成。本文详解 PagingSource、Pager、PagingDataAdapter 等核心组件,涵盖纯网络分页、RemoteMediator 离线缓存、加载状态展示与下拉刷新实战。
53 0
|
18天前
|
安全 数据库 Android开发
ContentProvider 跨进程数据共享实战
ContentProvider是Android跨进程数据共享标准方案:通过URI统一访问接口,支持CRUD操作、权限控制与数据变更监听。适用于共享联系人/相册等系统数据,或自建安全的数据服务,需注意线程安全与性能优化。
76 0
|
20天前
|
缓存 监控 Java
[Android 从零到一] Bitmap 内存管理与 OOM 治理:从崩溃现场到可控降级策略
本文深入剖析Android Bitmap内存管理与OOM治理:涵盖内存分配机制、inSampleSize采样降级、RGB_565格式优化、inBitmap复用池、Glide三级缓存策略,以及OOM监控、优雅降级和低内存设备适配等实战方案,助你构建稳定高效的图片加载体系。
86 0
|
24天前
|
移动开发 JavaScript 安全
Android WebView 治理实战:从线上白屏崩溃到协议化通信
本文针对电商App WebView白屏崩溃问题,系统梳理JSBridge通信、生命周期管理与异常兜底三大隐患,提出协议化+版本化JSBridge、域名白名单校验、SafeWebView生命周期治理及多层异常兜底方案,实现崩溃归零、白屏可重试、问题可定位可预防。
91 0