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

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

相关推荐
Henry_He1 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗1 小时前
Android笔试面试题AI答之Android基础(1)
android
qq_397562313 小时前
android studio更改应用图片,和应用名字。
android·ide·android studio
峥嵘life3 小时前
Android Studio版本升级那些事
android·ide·android studio
新手上路狂踩坑3 小时前
Android Studio的笔记--BusyBox相关
android·linux·笔记·android studio·busybox
TroubleMaker5 小时前
OkHttp源码学习之retryOnConnectionFailure属性
android·java·okhttp
叶羽西7 小时前
Android Studio IDE环境配置
android·ide·android studio
发飙的蜗牛'7 小时前
23种设计模式
android·java·设计模式
花追雨17 小时前
Android -- 双屏异显之方法一
android·双屏异显
小趴菜822717 小时前
安卓 自定义矢量图片控件 - 支持属性修改矢量图路径颜色
android