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能知道当前是第几次重试。

最直接的方法是给blocklambda增加一个当前第几次重试的参数,但是考虑到后面可能还会有更多扩展,我们可以定义一个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

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

相关推荐
每次的天空5 小时前
Android学习总结之算法篇五(字符串)
android·学习·算法
Gracker6 小时前
Android Weekly #202513
android
张拭心8 小时前
工作九年程序员的三月小结
android·前端
每次的天空8 小时前
Flutter学习总结之Android渲染对比
android·学习·flutter
鸿蒙布道师11 小时前
鸿蒙NEXT开发土司工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
智想天开11 小时前
11.使用依赖注入容器实现松耦合
android
yunteng52112 小时前
音视频(四)android编译
android·ffmpeg·音视频·x264·x265
tangweiguo0305198712 小时前
(kotlin) Android 13 高版本 图片选择、显示与裁剪功能实现
android·开发语言·kotlin
匹马夕阳12 小时前
(一)前端程序员转安卓开发分析和规划建议
android·前端
Kika写代码12 小时前
【Android】UI开发:XML布局与Jetpack Compose的全面对比指南
android·xml·ui