Kotlin 协程的高级用法

一、协程上下文与调度器

协程上下文的构成与组合

协程上下文(CoroutineContext) 是一组元素的集合,包括 Job、调度器、异常处理器等。

  • 主要元素类型

    • Job:控制协程生命周期
    • CoroutineDispatcher:指定协程运行线程
    • CoroutineName:协程名称,用于调试
    • CoroutineExceptionHandler:处理未捕获异常
  • 上下文组合 :使用 + 运算符组合多个上下文元素

kotlin 复制代码
val context = Dispatchers.Main + CoroutineName("NetworkRequest") + Job()
  • 上下文继承:协程从启动它的协程继承上下文,可通过参数覆盖

自定义协程上下文元素

实现自定义上下文元素需继承 AbstractCoroutineContextElement 或实现 CoroutineContext.Element 接口。

kotlin 复制代码
class TraceIdContext(val traceId: String) : AbstractCoroutineContextElement(Key) {
    companion object Key : CoroutineContext.Key<TraceIdContext>
}

// 使用自定义上下文
launch(Dispatchers.Main + TraceIdContext("trace-123")) {
    val traceId = coroutineContext[TraceIdContext]?.traceId
}

调度器的高级应用与切换策略

CoroutineDispatcher 决定协程在哪个线程或线程池执行。

  • 常用调度器

    • Dispatchers.Main:主线程,用于UI操作
    • Dispatchers.IO:IO密集型操作
    • Dispatchers.Default:CPU密集型计算
    • Dispatchers.Unconfined:非受限调度器
  • 调度器切换 :使用 withContext 临时切换调度器

kotlin 复制代码
suspend fun fetchData(): Data = withContext(Dispatchers.IO) {
    // IO操作
    apiService.getData()
}
  • 高级切换策略
    • 避免不必要的线程切换
    • 长时间运行的任务使用 Dispatchers.Default
    • 实现自定义调度器满足特定需求

二、协程作用域与结构化并发

自定义CoroutineScope实现

CoroutineScope 管理协程生命周期,确保协程不会泄漏。

  • 自定义作用域实现
kotlin 复制代码
class MyViewModel : ViewModel(), CoroutineScope {
    private val job = SupervisorJob()

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job + CoroutineName("MyViewModel")

    // 清除时取消所有协程
    override fun onCleared() {
        super.onCleared()
        job.cancel()
    }

    // 使用作用域启动协程
    fun loadData() {
        launch {
            // 执行任务
        }
    }
}

SupervisorJob与异常隔离

SupervisorJob 允许子协程失败而不影响其他子协程,实现异常隔离。

  • 与普通Job的区别
    • 普通Job:一个子协程失败,所有子协程和父协程都会被取消
    • SupervisorJob:一个子协程失败,不影响其他子协程和父协程
kotlin 复制代码
val supervisorScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

// 子协程失败不会取消其他子协程
supervisorScope.launch {
    // 子协程1
}
supervisorScope.launch {
    // 子协程2,失败不会影响子协程1
}
  • supervisorScope函数:创建一个使用SupervisorJob的作用域

协程生命周期管理与取消传播

  • 协程取消是协作式的:协程必须检查取消状态
  • 取消传播:父协程取消会传播给所有子协程
  • 生命周期管理实践
kotlin 复制代码
val scope = CoroutineScope(Dispatchers.Main + Job())

// 启动多个子协程
val job1 = scope.launch { ... }
val job2 = scope.launch { ... }

// 取消所有子协程
scope.cancel()

// 或取消单个协程
job1.cancel()
  • 检测取消 :使用 isActive 属性或 ensureActive() 函数
kotlin 复制代码
while (isActive) {
    // 执行循环任务
}

三、高级挂起函数与流

自定义挂起函数的实现模式

挂起函数 是可以暂停执行并稍后恢复的函数,使用 suspend 关键字修饰。

  • 回调转换模式 :使用 suspendCancellableCoroutine 将回调API转换为挂起函数
kotlin 复制代码
suspend fun fetchData(): Result = suspendCancellableCoroutine { continuation ->
    val call = apiService.makeRequest()
    call.enqueue(object : Callback<Result> {
        override fun onSuccess(result: Result) {
            continuation.resume(result)
        }

        override fun onFailure(e: Exception) {
            continuation.resumeWithException(e)
        }
    })

    // 处理取消
    continuation.invokeOnCancellation {
        call.cancel()
    }
}
  • 异步结果模式 :使用 coroutineScope 并发执行多个挂起函数
kotlin 复制代码
suspend fun loadAllData(): CombinedResult = coroutineScope {
    val data1 = async { fetchData1() }
    val data2 = async { fetchData2() }
    CombinedResult(data1.await(), data2.await())
}

Flow的高级操作符与背压处理

在实际开发中,Flow的高级操作符能够显著简化复杂数据流处理逻辑。例如,transform操作符相比map提供了更大的灵活性,可以在转换过程中发射多个值或跳过某些值:

kotlin 复制代码
flowOf(1, 2, 3)
    .transform { value ->
        if (value % 2 == 0) {
            emit("Even: $value")
        } else {
            emit("Odd: $value")
            emit("Odd squared: ${value * value}")
        }
    }
    .collect { println(it) }
// 输出: Odd: 1, Odd squared: 1, Even: 2, Odd: 3, Odd squared: 9

背压处理是Flow的核心优势之一,它解决了生产者和消费者处理速度不匹配的问题。Flow通过以下策略实现背压管理:

  1. 缓冲策略 :使用buffer()操作符为流设置缓冲区,允许生产者在消费者处理期间继续发射数据

    kotlin 复制代码
    flow {
        repeat(5) {
            delay(100) // 生产者较慢
            emit(it)
        }
    }
    .buffer(3) // 设置缓冲区大小
    .collect {
        delay(300) // 消费者较慢
        println(it)
    }
  2. 并发处理conflate()操作符会丢弃中间值,只保留最新值,适用于UI更新等场景

    kotlin 复制代码
    // 当新值产生时,如果前一个值还未被处理,则直接丢弃前一个值
    flow {
        repeat(10) {
            delay(100)
            emit(it)
        }
    }
    .conflate()
    .collect {
        delay(300)
        println(it)
    }
  3. 背压感知 :Flow的collectLatest操作符会在新值到达时取消当前正在处理的旧值,确保始终处理最新数据

    kotlin 复制代码
    flow {
        emit(1)
        delay(50)
        emit(2)
        delay(50)
        emit(3)
    }
    .collectLatest { value ->
        println("Processing $value")
        delay(100) // 模拟耗时处理
        println("Finished processing $value")
    }
    // 输出: Processing 1, Processing 2, Processing 3, Finished processing 3

Flow还提供了onBackpressureBuffer(), onBackpressureDrop()onBackpressureLatest()等操作符,允许开发者根据具体场景自定义背压处理策略,确保数据流处理的效率和稳定性。

Flow 是冷流,用于异步发射多个值,支持背压处理。

  • 常用高级操作符
    • transform:转换发射的值
    • combine/zip:组合多个流
    • debounce:防抖,忽略短时间内的频繁发射
    • distinctUntilChanged:仅发射与前一个不同的值
    • retry/retryWhen:失败时重试
kotlin 复制代码
val searchResults = searchQueryFlow
    .debounce(300)
    .distinctUntilChanged()
    .transform { query ->
        emit(Loading)
        try {
            val results = repository.search(query)
            emit(Success(results))
        } catch (e: Exception) {
            emit(Error(e))
        }
    }
    .flowOn(Dispatchers.IO)
  • 背压处理策略
    • 缓冲:buffer()
    • 合并:conflate()
    • 最新值:collectLatest()

Channel的高级应用与并发模式

Channel 用于协程间通信,支持发送和接收数据。

  • Channel类型

    • Rendezvous:无缓冲区,发送和接收需同时准备好
    • Buffered:有固定大小缓冲区
    • Unlimited:无界缓冲区
    • Conflated:只保留最新元素
  • 生产者-消费者模式

kotlin 复制代码
val channel = Channel<Int>(capacity = 10)

// 生产者
launch {
    for (i in 1..100) {
        channel.send(i)
    }
    channel.close()
}

// 消费者
launch {
    for (value in channel) {
        process(value)
    }
}
  • 使用produce和actor构建器
kotlin 复制代码
// 生产者协程
val producer = produce<Int> {
    repeat(10) { send(it) }
}

// 消费者协程
val actor = actor<Int> {
    for (msg in channel) {
        println("Received: $msg")
    }
}

四、协程异常处理与测试

异常传播与捕获机制

协程异常传播遵循特定规则,取决于协程的启动方式和作用域。

  • 异常传播路径

    • 直接使用 launch 启动的协程:异常向上传播给父协程
    • 使用 async 启动的协程:异常在调用 await() 时抛出
  • 异常捕获方式

    1. try/catch 块包裹挂起函数调用
    2. 使用 CoroutineExceptionHandler 捕获未处理异常
kotlin 复制代码
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    println("Caught exception: $exception")
}

launch(Dispatchers.Main + exceptionHandler) {
    riskyOperation() // 异常会被handler捕获
}

// 或使用try/catch
launch {
    try {
        riskyOperation()
    } catch (e: Exception) {
        // 处理异常
    }
}

协程作用域的错误处理策略

不同作用域对异常的处理方式不同:

  • 普通作用域:一个子协程异常会取消整个作用域
  • Supervisor作用域:子协程异常不会传播给其他子协程
kotlin 复制代码
// 异常隔离示例
supervisorScope {
    launch {
        // 此协程的异常不会影响其他协程
        throw RuntimeException("Failed in child")
    }
    launch {
        // 不受上述异常影响
        delay(1000)
        println("This still executes")
    }
}
  • 全局异常处理:为应用定义全局异常处理器,捕获未处理异常

协程测试框架与实践技巧

使用官方提供的 kotlinx-coroutines-test 库测试协程代码。

  • 测试调度器 :使用 TestDispatcher 控制时间和线程
kotlin 复制代码
@get:Rule
val coroutineRule = MainCoroutineRule() // 使用TestDispatcher

@Test
fun testDataLoading() = runTest {
    // 测试代码
    val viewModel = MyViewModel(repository)
    viewModel.loadData()

    advanceUntilIdle() // 执行所有挂起函数

    assertEquals(/* 验证结果 */)
}
  • 测试技巧
    • 使用 runTest 函数标记测试函数
    • 使用 advanceTimeBy 控制时间流逝
    • 使用 UnconfinedTestDispatcher 立即执行协程
    • 验证协程取消和异常情况
相关推荐
爷_1 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
charlee443 小时前
行业思考:不是前端不行,是只会前端不行
前端·ai
Amodoro4 小时前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin4 小时前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说4 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang4535 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2435 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
三口吃掉你5 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat
Trust yourself2435 小时前
在easyui中如何设置自带的弹窗,有输入框
前端·javascript·easyui
烛阴5 小时前
Tile Pattern
前端·webgl