协程的去抖动、节流、重试选项

69 篇文章 3 订阅

介绍

Kotlin 语言长期以来一直是 Android 应用程序开发的事实上的标准,这也就不足为奇了——空类型安全性、高阶函数和许多其他方便的机制帮助它赢得了开发人员的欢迎和喜爱。协程就是一种大大简化了异步代码工作的机制。

在本文中,通过向远程服务器发出请求的示例,将展示如何使用协程来调整异步操作随着时间的推移执行的顺序(去抖、限制、重试)。

Kotlin 中的协程

协程的概念

协程是轻量级执行线程,允许在同步功能内创建异步代码。它们提供了一种在 Kotlin 中组织竞争力的新机制,并允许开发人员简化异步编程,避免 Java 线程和回调 API 的问题。

协程可用于在单独的线程中执行任务,而不会阻塞主 (UI) 线程,从而使应用程序在出现错误时更具响应能力和容错能力。

使用协程的好处
  • • 简化异步代码:协程允许以更方便且易于阅读的形式编写异步代码,而不需要复杂的构造(例如 Callback 或 Promise)。

  • • 减少资源消耗:协程不会根据任务需要创建尽可能多的线程,从而减少了 CPU、内存和操作系统的负载。

  • • 避免阻塞主 UI 线程:协程可以与主线程并行运行而不会阻塞主线程,从而使应用程序响应更快,用户界面响应更灵敏、更流畅。

  • • 网络和磁盘 I/O 操作:协程允许在不阻塞线程的情况下执行阻塞操作,例如网络请求和磁盘 I/O 操作,从而减少完成操作的延迟。

创建和运行协程

要在 Kotlin 中使用协程,需要在 Gradle 文件中添加依赖:

dependencies {
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:$oroutinesVersion'
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion'
}

要运行协程,需要创建一个挂起函数:

suspend fun fetchData() {
   // Asynchronous code, such as network queries and databases, can be used here
}

挂起函数只能从协程或另一个挂起函数调用,因此特殊的协程构建器(launch 或 async)用于从同步执行切换到异步执行。这必须在特殊的 CoroutineScope 中完成,它控制在其中运行的协程的可见性和状态。大多数新版本的 Jetpack 库(Lifecycle、ViewModel 等)都添加了对相应 CoroutineScope 的支持:

class MainViewModel: ViewModel() {
  fun startFetchData() {
    viewModelScope.launch {
      fetchData()
    }
  }
}

使用协程处理网络请求

网络请求是 Android 应用程序中需要异步代码的主要任务之一。当应用程序发出网络请求时,它会等待服务器的响应,此时应用程序不应阻塞。否则,应用程序将挂起直到响应到达,这可能会导致响应能力和用户体验不佳。

在 Android 开发的不同时期,使用了不同的方法(Handler、Thread、AsymcTask、回调)来解决这个问题。然而,许多这些方法会导致代码难以阅读和理解。

Kotlin 协程通过为异步网络 I/O 操作创建轻量级执行线程来避免这些问题;该代码看起来就像普通的同步代码。

请求执行机制

通常需要灵活配置操作执行的频率。要将这些方法与 Kotlin 协程一起使用,可以使用标准 Kotlin 库“kotlinx.coroutines.flow”中的函数。接下来,看一下使用 debounce、throttle 和 retryWhen 函数的示例。

debounce 去抖动

对函数进行去抖动意味着所有调用都将被忽略,直到它们停止一段时间。只有这样该函数才会被调用。例如,如果我们将计时器设置为 2 秒,并且该函数以 1 秒的间隔调用 10 次,则实际调用将在最后一次(第十次)调用该函数后仅 2 秒发生。

图片

kotlinx.coroutines.flow 中的 debounce 方法允许您在将数据释放到流中之前设置延迟,以避免对使用者(通常是用户界面)的频繁更新。

该方法的本质是在接收到最后一个流值后会等待一定的时间间隔。如果在此期间收到新值,则间隔将重新开始。如果在等待间隔期间没有收到新值,则最后一个值将输出到最终线程。

例如,如果使用一个线程在每次用户输入字符时更新电话查找,那么使用 debounce 方法,电话将不会随着每次更改而不断出现和消失。相反,在用户完成键入后,更新查找会延迟一定时间。

假设我们的任务是处理用户在搜索字段中的输入,但只有在用户完成输入数据后才需要将数据发送到服务器。在这种情况下,可以使用 debounce 方法,仅在用户输入完成后才处理用户的输入。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
 val searchQuery = MutableStateFlow("")
 
 // set a delay of 500 ms before each retrieved value
 val result = searchQuery.debounce(500)
 
 // subscribing to receive values from the result
 result.collect { 
   // send a request to the server only if the result of the search string is not empty
   if (it.isNotBlank()) {
     println("Request to server: $it")
   } else {
     println("The search string is empty")
   }
 }
 
 // simulate user input in the search field
 launch {
 delay(0)
 searchQuery.value = "a"
 delay(100)
 searchQuery.value = "ap"
 delay(100)
 searchQuery.value = "app"
 delay(1000)
 searchQuery.value = "apple"
 delay(1000)
 searchQuery.value = ""
 }
}

在此示例中,搜索字段由 MutableStateFlow 表示。去抖方法在检索每个值之前设置 0.5 秒的延迟。在最后一个线程中,仅当搜索字符串不为空时才会向服务器发出请求。

执行此代码的结果将打印到控制台:

Request to server: app
Request to server: apple
The search string is empty

这意味着仅在用户在搜索字段中输入完数据后才向服务器发送请求。

Throttling 节流

图片

函数限制是指在指定时间段内调用该函数不超过一次(例如每 10 秒一次)。换句话说,如果某个函数最近已经被调用过,trattting 会阻止该函数被调用。 Trattting 还确保该函数定期执行。

throttle 方法与 debounce 类似,因为它也用于控制线程内发送项目的频率。这些方法之间的区别在于它们如何处理接收到的值。与防抖不同的是,防抖会忽略除最后一个值之外的所有值,而节流阀会记住最后一个值,并在设置的时间间隔到期时重置计时器。如果没有出现新值,则将最后存储的值发送到输出流。

因此,debounce 会忽略除最后一个值之外的所有值,并仅在经过一定时间后发送最后一个值,而不会收到新值。 Throttle 会记住最后一个值,并在每次一定时间间隔后发送它,无论该时间段内收到的值数量如何。

两种方法都适用于不同的场景,方法的选择取决于所需的数据处理逻辑。

Retry 重试

使用 Kotlin 协程,如果上次操作失败,可以在一段时间后轻松重试操作。为此,可以使用 retryWhen 函数,该函数允许确定应重复操作的频率和次数。

retryWhen 函数应与标准 Kotlin 库“kotlinx.coroutines”中的 catch 语句结合使用。 catch 语句用于捕获操作期间可能发生的任何异常。

但是,retryWhen 方法是作为 Flow 的扩展来实现的。要允许操作独立于流程执行一次,请考虑其自己的实现:

suspend fun loadResource(url: String): Resource {
 // loadResource by url
}

suspend fun getResourceWithRetry(url: String, retries: Int, intervalMillis: Long): Resource {
 return try {
   loadResource(url)
 } catch (e: Exception) {
   if (retries > 0) {
     delay(intervalMillis) // a delay for a certain period of time
     getResourceWithRetry(url, retries - 1, intervalMillis) // repeat the operation after a certain period of time
   } else {
     throw e // throw an exception if retries are expired
   }
 }
}

// example of use
CoroutineScope(Dispatchers.IO).launch {
 val resource = getResourceWithRetry("http://example.com/resource", 3, 1000)
 // use of the loaded resource
}

这里我们定义了一个 getResourceWithRetry 函数,它调用 loadResource 操作来加载给定 URL 处的资源。如果操作不成功,将使用延迟函数递归调用函数。

尝试重复操作的次数由retries参数决定,重试之间的时间间隔由intervalMillis参数决定。

为了处理异常,在 loadResource 函数调用周围使用 catch 语句。如果操作失败,会再次调用 getResourceWithRetry 函数,重试次数减少 1,并延迟 IntervalMillis 参数定义的时间间隔。

这样,如果上次失败,可以使用 retryWhen 函数和 catch 运算符轻松地在一段时间后重新运行该操作。

所提出的算法有两个缺点:它执行一个固定操作(访问网络中的特定地址)并且以恒定的间隔执行。第一个问题可以通过使用模板方法来解决,该方法允许用必要的操作来代替可能失败的代码调用。第二个问题可以通过稍微复杂地计算尝试之间的间隔来解决(这尤其重要,例如,在访问远程服务器时 - 如果许多客户端以相同的间隔重复尝试,则存在增加服务器上的负载达到临界水平)。

因此,我们有一个更灵活的选择:

suspend fun <T> getResourceWithRetry(
 retries: Int = 5, // 5 retries
 initialDelay: Long = 100L, // 0.1 second
 maxDelay: Long = 128000L, // 128 seconds
 factor: Double = 2.0,
 block: suspend () -> T): T
{
 return try {
   loadResource(url)
 } catch (e: Exception) {
   if (retries > 0) {
     val currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
     delay(currentDelay) // delay for a certain period of time
     getResourceWithRetry(retries - 1, currentDelay, maxDelay, factor, block) // repeat the operation after some time
   } else {
     throw e // throw an exception if retries are over
   }
}

结论

总之, Kotlin 协程操作是用于控制事件的计时和排序的强大工具,可帮助您解决与应用程序中的异步和数据流管理相关的各种问题。使用这些操作可以显着提高代码的质量和效率,改善用户体验,并减少与异步相关的错误。

但是,在使用这些操作时,您应该记住它们有自己的特殊性,并且需要谨慎使用。例如,值得考虑对尝试次数和尝试之间的时间间隔进行限制,以免导致无休止的重试循环、产生过载,或者相反,跳过操作(值)。

转自:协程的去抖动、节流、重试选项

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值