Kotlin 协程:使用与原理详解

一、Kotlin 协程基本概念

1. 什么是协程

协程(Coroutine)是 Kotlin 提供的一种轻量级线程管理框架,它允许以顺序的方式编写异步代码,同时提供了高效的并发处理能力。

核心特点

  • 轻量级:比线程更高效,可以创建数千个协程而不会导致性能问题
  • 结构化并发:提供明确的生命周期管理和取消机制
  • 挂起机制:可以在不阻塞线程的情况下暂停和恢复执行

2. 协程 vs 线程

特性协程线程
创建成本非常低(字节级)较高(MB级栈内存)
切换开销几乎为零需要内核介入,开销较大
并发数量可轻松创建数万个通常数百个就达到极限
阻塞行为挂起不阻塞线程阻塞会占用线程资源

二、协程的基本使用

1. 添加依赖

1
2
3
4
5
// build.gradle (Module)
dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
}

2. 启动协程的三种方式

(1) launch - 启动不需要返回结果的协程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 在Activity或ViewModel中
lifecycleScope.launch {
    // 在主线程执行
    val result = fetchData() // 挂起函数
    updateUI(result)
}

private suspend fun fetchData(): String {
    return withContext(Dispatchers.IO) {
        // 在IO线程执行
        delay(1000) // 模拟耗时操作
        "Data loaded"
    }
}

(2) async - 启动需要返回结果的协程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
lifecycleScope.launch {
    val deferred1 = async { fetchData1() }
    val deferred2 = async { fetchData2() }
    
    // await()是挂起函数,不会阻塞线程
    val result1 = deferred1.await()
    val result2 = deferred2.await()
    
    showResults(result1, result2)
}

(3) runBlocking - 阻塞当前线程的协程(主要用于测试)

1
2
3
4
5
6
7
8
fun main() = runBlocking {
    // 会阻塞当前线程直到协程完成
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

3. 协程调度器(Dispatchers)

调度器用途
Dispatchers.MainAndroid主线程更新UI
Dispatchers.IO磁盘/网络IO操作
Dispatchers.DefaultCPU密集型计算任务
Dispatchers.Unconfined不限定特定线程(不推荐常规使用)
1
2
3
4
5
6
7
8
9
viewModelScope.launch(Dispatchers.IO) {
    // 在IO线程执行
    val data = repository.loadData()
    
    withContext(Dispatchers.Main) {
        // 切换回主线程更新UI
        _uiState.value = UiState.Success(data)
    }
}

三、协程的核心原理

1. 挂起函数(Suspend Function)原理

挂起函数是协程的核心机制,编译器会对挂起函数进行特殊处理:

1
2
3
4
suspend fun fetchUser(): User {
    val user = api.fetchUser() // 另一个挂起函数
    return user
}

编译后的伪代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Object fetchUser(Continuation<User> continuation) {
    // 状态机实现
    when(continuation.label) {
        0 -> {
            continuation.label = 1
            api.fetchUser(continuation)
            return COROUTINE_SUSPENDED
        }
        1 -> {
            val user = continuation.result as User
            return user
        }
    }
}

2. 协程的 CPS (Continuation Passing Style) 变换

Kotlin 编译器会将挂起函数转换为状态机,通过 Continuation 回调实现挂起和恢复:

  1. 每次挂起点(suspend point)都会成为状态机的一个状态
  2. 挂起时保存当前状态和局部变量
  3. 恢复时从保存的状态继续执行

3. 协程上下文(CoroutineContext)

协程上下文包含以下关键元素:

  • Job:控制协程的生命周期和取消
  • CoroutineDispatcher:决定协程在哪个线程执行
  • CoroutineName:协程的名称(调试用)
  • CoroutineExceptionHandler:异常处理
1
2
3
4
5
val customContext = Dispatchers.IO + CoroutineName("MyCoroutine") + exceptionHandler

lifecycleScope.launch(customContext) {
    // 使用自定义上下文
}

4. 结构化并发

Kotlin 协程通过以下机制实现结构化并发:

  • 父协程-子协程关系:父协程会等待所有子协程完成
  • 取消传播:取消父协程会自动取消所有子协程
  • 异常传播:子协程异常会传播给父协程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
val parentJob = CoroutineScope(Dispatchers.Main).launch {
    // 父协程
    launch {
        // 子协程1
        delay(1000)
    }
    launch {
        // 子协程2
        delay(2000)
    }
}

// 取消父协程会同时取消两个子协程
parentJob.cancel()

四、Android 中的协程最佳实践

1. 使用 Jetpack 提供的协程作用域

1
2
3
4
5
6
7
8
9
// Activity/Fragment
lifecycleScope.launch {
    // 自动跟随生命周期取消
}

// ViewModel
viewModelScope.launch {
    // 当ViewModel cleared时自动取消
}

2. 正确处理协程异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 方式1:try-catch
lifecycleScope.launch {
    try {
        fetchData()
    } catch (e: Exception) {
        showError(e)
    }
}

// 方式2:CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
    Log.e("Coroutine", "Caught $exception")
}

lifecycleScope.launch(handler) {
    throw RuntimeException("Test exception")
}

3. 避免内存泄漏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 错误示例:可能泄漏Activity
class MyActivity : AppCompatActivity() {
    private var job: Job? = null
    
    override fun onCreate() {
        job = GlobalScope.launch {
            // 即使Activity销毁,协程仍继续运行
            fetchData()
        }
    }
}

// 正确示例:使用lifecycleScope
class MyActivity : AppCompatActivity() {
    override fun onCreate() {
        lifecycleScope.launch {
            // 当Activity销毁时自动取消
            fetchData()
        }
    }
}

4. 协程与 Retrofit 结合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): User // 挂起函数
}

// ViewModel中
fun loadUser(userId: String) {
    viewModelScope.launch {
        try {
            _uiState.value = UiState.Loading
            val user = repository.getUser(userId)
            _uiState.value = UiState.Success(user)
        } catch (e: Exception) {
            _uiState.value = UiState.Error(e)
        }
    }
}

五、协程高级主题

1. 通道(Channel)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
val channel = Channel<Int>()

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

// 消费者
lifecycleScope.launch {
    for (value in channel) {
        println("Received $value")
    }
}

2. 流(Flow)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun fetchDataFlow(): Flow<String> = flow {
    for (i in 1..5) {
        delay(1000)
        emit("Data $i")
    }
}

// 收集流
lifecycleScope.launch {
    fetchDataFlow()
        .flowOn(Dispatchers.IO) // 指定上游执行上下文
        .catch { e -> println("Error: $e") }
        .collect { value ->
            // 在主线程接收数据
            updateUI(value)
        }
}

3. 协程测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Test
fun testCoroutine() = runTest { // 使用TestCoroutineScope
    val repository = TestRepository()
    
    // 启动协程
    val job = launch {
        repository.loadData()
    }
    
    // 控制虚拟时间
    advanceTimeBy(1000)
    
    // 验证结果
    assertEquals("Test Data", repository.result)
    
    // 确保协程完成
    job.cancel()
}

六、协程性能优化

  1. 避免过度切换调度器:减少 withContext 切换
  2. 合理使用协程作用域:避免不必要的协程存活
  3. 适当使用 flowOn:减少 Flow 的线程切换
  4. 使用 supervisorScope:当不希望子协程异常影响其他子协程时
  5. 合理设置协程上下文:避免不必要的上下文元素

总结

Kotlin 协程为 Android 开发提供了强大的异步编程解决方案:

  • 简化异步代码:用同步方式写异步逻辑
  • 高效线程管理:减少线程切换开销
  • 结构化并发:避免内存泄漏和资源浪费
  • 与Jetpack深度集成lifecycleScopeviewModelScope

理解协程的原理和最佳实践,可以帮助开发者编写更高效、更健壮的 Android 应用。

Built with Hugo
主题 StackJimmy 设计