You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin协程retryIO失败捕获异常、重试次数不生效问题排查

问题根因

你遇到的两个问题本质是两个逻辑缺陷叠加导致的:

  • 定时器逻辑导致重试任务重叠:你用的java.util.Timer是固定10秒触发一次任务,完全不感知协程执行状态,也不管上一次健康检查的重试流程有没有跑完。结合你接口10秒左右的超时配置,每个定时器触发时,上一次启动的重试任务刚跑完第一次接口调用(超时失败打retryIO repeat 0 failed日志),新的重试任务就已经启动了。你日志里大量的repeat 0记录根本不是同一个重试任务的多次尝试,是每10秒新开的重试任务各自打出来的第一次失败日志,旧任务要么被后续逻辑打断,要么因为协程没有绑定生命周期被回收,根本跑不完3次重试流程,自然不会打印retryIO run final block日志。
  • 异常捕获范围不全:retryIO方法内仅捕获IOException,但实际网络请求场景下可能抛出非IO类型的异常(比如协程取消异常、序列化异常、部分框架封装的网络异常等),这类异常不会被重试块内的catch捕获,会直接中断重试流程;同时你每次调用都新建独立的CoroutineScope,没有配置全局异常处理器,未捕获的异常不会走到你写的外层catch逻辑,自然无法触发断连状态的UI更新。
修复方案

按以下步骤调整即可完全符合预期:

  1. 优化retryIO方法,扩大异常捕获范围,支持自定义可重试异常判断,避免异常漏抓
  2. 废弃java.util.Timer,改用协程原生的顺序轮询逻辑,保证上一次健康检查(含完整重试流程)执行完毕后,再等待间隔发起下一次检查,从根源上避免任务重叠
  3. 使用和页面生命周期绑定的协程域(如lifecycleScope)启动任务,避免内存泄漏,保证页面销毁时轮询自动终止

修复后的retryIO实现

suspend fun <T> retryIO(
    times: Int = 3,
    initialDelay: Long = 100, // 0.1秒
    maxDelay: Long = 1000,    // 1秒
    factor: Double = 2.0,
    retryOn: (Throwable) -> Boolean = { it is IOException }, // 可自定义需要重试的异常规则
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) { attempt ->
        try {
            return block()
        } catch (e: Throwable) {
            // 不可重试的异常直接抛出,终止重试
            if (!retryOn(e)) throw e
            Timber.i("retryIO 第${attempt + 1}次调用失败,${currentDelay}ms后重试")
            Timber.e(e)
        }
        delay(currentDelay)
        currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
    }
    Timber.i("retryIO 执行最后一次尝试")
    // 最后一次尝试不捕获异常,失败直接向外抛出
    return block()
}

修复后的健康检查轮询逻辑

// 用页面生命周期绑定的lifecycleScope启动,页面销毁时自动取消轮询,无需手动处理
lifecycleScope.launch {
    var isFirstRequest = true
    while (isActive) {
        if (!isFirstRequest) delay(10000L) // 非首次请求,等待10秒轮询间隔
        isFirstRequest = false
        try {
            // 切换到IO线程执行网络请求
            val result = withContext(Dispatchers.IO) {
                retryIO(times = 3) {
                    api.doGetHealth().await()
                }
            }
            Timber.i("健康检查成功")
            // 切换到主线程更新UI
            withContext(Dispatchers.Main) {
                updateHealthStatus(HealthStatus.CONNECTED)
            }
        } catch (e: Exception) {
            Timber.i("健康检查重试全部失败")
            Timber.e(e)
            withContext(Dispatchers.Main) {
                updateHealthStatus(HealthStatus.DISCONNECTED)
            }
        }
    }
}
修复效果说明
  • 不会再出现大量重复的repeat 0日志:轮询逻辑是顺序执行的,同一时间只会有一个健康检查任务在跑,重试次数严格按照传入的times参数执行
  • 可以正常打印最后一次尝试的日志:没有重叠任务打断,重试流程可以完整执行
  • 异常可以被正常捕获:所有重试失败后抛出的异常会被外层catch块捕获,正常触发断连状态的UI更新
  • 无内存泄漏风险:协程和页面生命周期绑定,页面退出后轮询自动终止,不会产生孤儿协程占用资源

内容的提问来源于stack exchange,提问作者chancyWu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 05:31:36