一、协程上下文与调度器
协程上下文的构成与组合
协程上下文(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通过以下策略实现背压管理:
-
缓冲策略 :使用
buffer()
操作符为流设置缓冲区,允许生产者在消费者处理期间继续发射数据kotlinflow { repeat(5) { delay(100) // 生产者较慢 emit(it) } } .buffer(3) // 设置缓冲区大小 .collect { delay(300) // 消费者较慢 println(it) }
-
并发处理 :
conflate()
操作符会丢弃中间值,只保留最新值,适用于UI更新等场景kotlin// 当新值产生时,如果前一个值还未被处理,则直接丢弃前一个值 flow { repeat(10) { delay(100) emit(it) } } .conflate() .collect { delay(300) println(it) }
-
背压感知 :Flow的
collectLatest
操作符会在新值到达时取消当前正在处理的旧值,确保始终处理最新数据kotlinflow { 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()
时抛出
- 直接使用
-
异常捕获方式:
try/catch
块包裹挂起函数调用- 使用
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
立即执行协程 - 验证协程取消和异常情况
- 使用