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 立即执行协程
    • 验证协程取消和异常情况
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax