Android - 重试逻辑封装

实际开发中,经常需要"重试"这个逻辑,例如:Http请求,发送IM消息,播放视频,直播推流,拉流等等。一般框架本身会提供重试方案,但是他们不统一,可能不好自定义重试策略。

重试应该是一套独立的逻辑,它不关心处理什么任务,只关心重试的逻辑本身。得益于Kotlin协程,可以轻松的封装重试逻辑。

封装

1.0版本

有bug的版本:

kotlin 复制代码
suspend fun <T> retry(
  /** 最大执行次数 */
  maxCount: Int = 3,
  /** 重试间隔 */
  delay: Long = 5000,
  /** 执行回调 */
  block: suspend () -> T,
): Result<T> {
  // 当前第几次执行
  var currentCount = 0
  while (true) {
     // 增加次数
     currentCount++

     // 执行block
     val result = runCatching {
        block()
     }

     // 如果成功,返回结果
     if (result.isSuccess) {
        return result
     }

     if (currentCount >= maxCount) {
        // 达到最大执行次数
        val exception = checkNotNull(result.exceptionOrNull())
        return Result.failure(RetryMaxCountException(exception))
     } else {
        // 延迟后继续执行
        delay(delay)
        continue
     }
  }
}

/** 达到最大执行次数异常 */
class RetryMaxCountException(cause: Throwable) : Exception(cause)

整体逻辑比较简单:循环执行block代码块,如果成功的话就返回结果,如果失败的话就重试,达到最大次数时,返回失败结果,结果的异常是RetryMaxCountException,并包含最后一次失败的真实异常。

上面的代码有bug:执行block的时候捕获了所有异常,包括协程取消异常CancellationException,导致协程不能被及时的取消,改进一下代码:

kotlin 复制代码
val result = runCatching {
   block()
}.onFailure { e ->
   // 取消异常,继续传播
   if (e is CancellationException) throw e
}

修改好了,但还是有问题:block里面可能没有抛出取消异常,而调用retry函数的协程已经被取消了,此时应该及时取消,而不是返回一个成功的结果,再改进一下代码:

kotlin 复制代码
// 确保当前协程处于`active`状态,否则抛出取消异常
currentCoroutineContext().ensureActive()

if (result.isSuccess) {
   return result
}

在每次block执行完成之后,调用currentCoroutineContext().ensureActive()确保当前协程处于active状态,否则抛出取消异常。

1.1版本

1.0版本的雏形已经写好了,但是实际使用中,我们可能会根据当前是第几次重试,做一些额外的业务逻辑,所以要让block能知道当前是第几次重试。

最直接的方法是给block的lambda增加一个当前第几次重试的参数,但是考虑到后面可能还会有更多扩展,我们可以定义一个RetryScope:

kotlin 复制代码
interface RetryScope {
   /** 当前执行次数 */
   val currentCount: Int
}

再把参数修改成这样子:

kotlin 复制代码
suspend fun <T> retry(
   // ...,
   block: suspend RetryScope.() -> T,
): Result<T>

最后,当然要写一下RetryScope的实现类,并在RetryScope的作用域中调用block。

实现类:

kotlin 复制代码
private class RetryScopeImpl : RetryScope {
   private var _count = 0
   override val currentCount: Int get() = _count
   fun increaseCount() = _count++
}

在RetryScope的作用域中调用block:

javascript 复制代码
with(RetryScopeImpl()) {
   while (true) {
      // 增加次数
      increaseCount()
       
      // ...
   }
}

修改好了,这样子我们在block中就能直接调用currentCount查询当前是第几次重试了。

1.2版本

目前为止,重试间隔是固定的,但有时候我们希望根据第几次重试,改变重试间隔,例如第一次是5秒,第二次是10秒,第三次是15秒,或者其他算法。

首先修改一下获取延迟间隔的参数,把delay参数修改为一个lambda:

kotlin 复制代码
suspend fun <T> retry(
   // ...,
   /** 获取延迟毫秒 */
   getDelay: RetryScope.() -> Long = { 5_000 },
   // ...,
): Result<T> 

调用getDelay获取延迟间隔:

kotlin 复制代码
if (currentCount >= maxCount) {
   // ...
} else {
   // 延迟后继续执行
   delay(getDelay())
   continue
}

这样子在getDelay中也能查询到当前第几次重试了,并根据第几次重试灵活设置重试间隔。

1.3版本

需求又来了,有时候我们不希望在任何异常情况下都重试,也就是说失败的时候要根据具体的失败异常,来决定要不要继续重试。

很显然,我们需要新增一个失败的回调参数,通过回调的返回值决定要不要继续重试:

kotlin 复制代码
suspend fun <T> retry(
   // ...,
   /** 失败回调 */
   onFailure: RetryScope.(Throwable) -> Boolean = { true },
   // ...,
): Result<T> {
    // ...
    val exception = checkNotNull(result.exceptionOrNull())
    val shouldContinue = onFailure(exception).also{currentCoroutineContext().ensureActive() }
    if (!shouldContinue) {
       return result
    }
    // ...
}

在失败的时候,先通知onFailure回调并把异常传给它,根据它的返回值,true:继续执行下面的逻辑;false:返回当前的失败结果。

注意:onFailure执行完成之后,还是要调用currentCoroutineContext().ensureActive()一下,因为我们不能确定onFailure里面到底发生了什么。

到此为止,算是一个比较完整的代码了,看一下该函数完整代码:

kotlin 复制代码
suspend fun <T> retry(
   maxCount: Int = 3,
   getDelay: RetryScope.() -> Long = { 5_000 },
   onFailure: RetryScope.(Throwable) -> Boolean = { true },
   block: suspend RetryScope.() -> T,
): Result<T> {
   require(maxCount > 0)
   with(RetryScopeImpl()) {
      while (true) {
         // 增加次数
         increaseCount()

         val result = runCatching {
            block()
         }.onFailure { e ->
            if (e is CancellationException) throw e
         }

         currentCoroutineContext().ensureActive()
         
         if (result.isSuccess) {
            return result
         }

         val exception = checkNotNull(result.exceptionOrNull())
         val shouldContinue = onFailure(exception).also { currentCoroutineContext().ensureActive() }
         if (!shouldContinue) {
            return result
         }

         if (currentCount >= maxCount) {
            // 达到最大执行次数
            return Result.failure(RetryMaxCountException(exception))
         } else {
            // 延迟后继续执行
            delay(getDelay())
            continue
         }
      }
   }
}

扩展

用模拟代码演示一下使用:

kotlin 复制代码
lifecycleScope.launch {
   retry {
      // 模拟请求数据
      requestData()
   }.onSuccess { data ->
      // 处理成功逻辑
   }.onFailure { error ->
      // 处理失败逻辑
   }
}

然而实际情况是,大部分需要重试的场景一般都需要网络,可以再封装一个netRetry函数,要求网络已连接才执行:

kotlin 复制代码
suspend fun <T> netRetry(
   maxCount: Int = 3,
   getDelay: RetryScope.() -> Long = { 5_000 },
   onFailure: RetryScope.(Throwable) -> Boolean = { shouldRetry(it) },
   block: suspend RetryScope.() -> T,
): Result<T> {
   return retry(
      maxCount = maxCount,
      getDelay = getDelay,
      onFailure = onFailure,
      block = {
         // 如果网络不通畅,则挂起,等待网络通畅恢复
         fAwaitNetwork()
         block()
      },
   )
}

private fun shouldRetry(throwable: Throwable): Boolean {
   if (throwable is SocketTimeoutException) return true
   return !FNetwork.currentNetwork.isConnected
}

有了前面的封装,这里的代码就比较简单,主要是挂起函数fAwaitNetwork,这个函数调用的时候会先检查网络是否已连接,如果已连接则不会挂起,如果网络未连接的话会挂起,直到网络已连接。

shouldRetry的返回值表示是否需要继续执行后面的逻辑,读者可以根据实际业务修改。

关于监听网络,和这个fAwaitNetwork函数的具体实现,可以查看这篇文章

结束

完整代码在这里:retry-ktx

感谢你的阅读,如果有问题欢迎一起交流学习。

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui