Android Kotlin 协程原理分析

14 阅读6分钟

Android Kotlin 协程原理分析

协程原理、异常机制、结构化并发,结合 Android 代码说明


一、协程原理

1.1 无栈协程与状态机

Kotlin 协程是无栈协程,不依赖线程栈,通过状态机实现挂起与恢复:

  • 挂起:释放当前线程,保存执行状态
  • 恢复:在任意线程上根据保存的状态继续执行

1.2 CPS 转换(Continuation-Passing Style)

编译器将 suspend 函数转换为带 Continuation 回调的形式:

// 源码
suspend fun fetchUser(id: Int): User {
    delay(1000)
    return api.getUser(id)
}

// 编译器转换后(伪代码)
fun fetchUser(id: Int, continuation: Continuation<User>): Any {
    when (continuation.label) {
        0 -> {
            continuation.label = 1
            if (delay(1000, continuation) == COROUTINE_SUSPENDED)
                return COROUTINE_SUSPENDED
        }
        1 -> {
            val user = api.getUser(id)
            continuation.resume(user)
            return user
        }
    }
}
  • 自动添加 Continuation 参数
  • label 标记挂起点
  • 挂起时返回 COROUTINE_SUSPENDED,恢复时调用 continuation.resumeWith()

1.3 状态机执行流程

// Android 示例:ViewModel 中发起网络请求
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val user = fetchUser(1)      // 挂起点 1
            val posts = fetchPosts(user) // 挂起点 2
            updateUI(posts)
        }
    }

    private suspend fun fetchUser(id: Int): User = withContext(Dispatchers.IO) {
        delay(500)
        api.getUser(id)
    }

    private suspend fun fetchPosts(user: User) = withContext(Dispatchers.IO) {
        api.getPosts(user.id)
    }
}

执行过程

  1. launch 创建协程,首次执行到 fetchUser 时,内部 delay 挂起,返回 COROUTINE_SUSPENDED
  2. 协程释放线程,状态机保存 label = 1
  3. delay 到期后,调度器调用 continuation.resumeWith()
  4. 状态机从 label = 1 继续,执行 fetchPosts
  5. 依此类推,直到协程结束

1.4 Continuation 接口

// kotlinx.coroutines 中的 Continuation
interface Continuation<in T> {
    val context: CoroutineContext  // 协程上下文(调度器、Job 等)
    fun resumeWith(result: Result<T>)  // 恢复执行,传入结果或异常
}
  • context:保存调度器、Job、异常处理器等
  • resumeWith:挂起函数完成时由框架调用,继续执行后续代码

1.5 挂起不阻塞线程

// Activity 中:主线程启动协程,挂起时主线程可处理其他任务
lifecycleScope.launch {
    val data = withContext(Dispatchers.IO) {
        repository.fetchData()  // 切到 IO 线程,主线程释放
    }
    textView.text = data  // 切回主线程更新 UI
}

二、协程异常原理

2.1 异常传播方式

构建器异常传播说明
launch自动向上传播异常会传递给父协程,最终到 CoroutineExceptionHandler
async向用户暴露异常在 await() 时抛出,需 try-catch 或 CoroutineExceptionHandler
supervisorScope隔离子协程异常不传播给兄弟协程和父协程

2.2 launch 异常传播

// 异常会向上传播,若未处理则导致协程取消
viewModelScope.launch {
    throw RuntimeException("error")  // 传播到 viewModelScope
}
// viewModelScope 会取消,并调用 CoroutineExceptionHandler(若配置)

2.3 async 异常暴露

// async 中异常在 await 时抛出
viewModelScope.launch {
    val deferred = async { fetchData() }  // 内部异常暂不抛出
    try {
        val result = deferred.await()  // 这里可能抛出异常
        updateUI(result)
    } catch (e: Exception) {
        showError(e.message)
    }
}

2.4 CoroutineExceptionHandler

// 全局捕获未处理的协程异常(仅对根协程有效)
val handler = CoroutineExceptionHandler { _, throwable ->
    Log.e("Coroutine", "未捕获异常", throwable)
}

lifecycleScope.launch(handler) {
    throw RuntimeException("test")  // 被 handler 捕获,不会崩溃
}

注意CoroutineExceptionHandler 必须作为根协程的 context 才能生效;子协程的异常会传播到根协程,由根协程的 handler 处理。

2.5 SupervisorJob 与 supervisorScope

// SupervisorJob:子协程失败不影响兄弟和父协程
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

scope.launch {
    launch { throw RuntimeException("A 失败") }  // A 失败
    launch { delay(1000); println("B 完成") }   // B 不受影响,继续执行
}

// supervisorScope:在挂起函数中创建隔离作用域
suspend fun loadMultiple() = supervisorScope {
    val a = async { fetchA() }  // 若失败,不影响 b
    val b = async { fetchB() }
    listOf(a.await(), b.await())
}

2.5.1 SupervisorJob 如何做到「子协程失败不影响兄弟和父协程」

核心机制:重写 childCancelled,返回 false 阻止异常向上传播。

// kotlinx.coroutines 源码(JobSupport.kt 简化)
class SupervisorJobImpl(parent: Job?) : JobImpl(parent), CompletableJob {
    override fun childCancelled(cause: Throwable): Boolean {
        return false  // 关键:不向父级传播,父 Job 和兄弟 Job 不受影响
    }
}

普通 Job 的行为

  • 子协程抛异常时,会调用父 Job 的 childCancelled(cause)
  • 普通 Job 返回 true,表示「已处理」,异常会继续向上传播,导致父 Job 和所有兄弟 Job 被取消
  • 即「一个失败,全部取消」

SupervisorJob 的行为

  • 重写 childCancelled 并返回 false
  • 表示「不向上传播」,父 Job 认为子协程的失败已在本层处理
  • 父 Job 和兄弟 Job 不会被取消,只有抛出异常的那个子协程自己失败

传播流程对比

普通 Job:
  子 A 失败 → childCancelled 返回 true → 父 Job 取消 → 兄弟 B、C 一并取消

SupervisorJob:
  子 A 失败 → childCancelled 返回 false → 父 Job 不取消 → 兄弟 B、C 继续执行

注意:SupervisorJob 只隔离直接子协程。若在子协程内部再 launch,内层协程的父 Job 是中间层 Job,不是 SupervisorJob。中间层 Job 失败仍会向上传播,可能取消其父协程。要让内层也隔离,需在对应层级使用 supervisorScopeSupervisorJob

2.6 Android 中的 SupervisorJob

// lifecycleScope 和 viewModelScope 内部都使用 SupervisorJob
// lifecycleScope 源码(简化)
val LifecycleOwner.lifecycleScope: CoroutineScope
    get() = lifecycle.coroutineScope  // 内部: SupervisorJob() + Dispatchers.Main.immediate

// viewModelScope 源码(简化)
val ViewModel.viewModelScope: CoroutineScope
    get() = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

因此,在 viewModelScope.launch { } 中,一个子协程抛异常不会取消其他子协程。

2.7 正确使用 try-catch

// 在协程内部捕获
viewModelScope.launch {
    try {
        val data = repository.fetchData()
        _uiState.value = UiState.Success(data)
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e.message)
    }
}

// 注意:CancellationException 应重新抛出,不要吞掉
viewModelScope.launch {
    try {
        delay(1000)
    } catch (e: CancellationException) {
        throw e  // 必须重新抛出,否则取消无法正确传播
    }
}

三、结构化并发原理

3.1 核心思想

结构化并发:所有协程必须在明确的作用域内启动,形成父子关系,父协程负责管理子协程的生命周期。

  • 无孤立协程:每个协程都有父协程
  • 取消传播:父协程取消时,所有子协程自动取消
  • 等待完成:父协程会等待所有子协程完成后才结束

3.2 父子关系

// launch 和 async 创建的子协程会继承父协程的 Job
viewModelScope.launch {           // 父协程
    launch { task1() }            // 子协程 1
    launch { task2() }            // 子协程 2
    // 父协程会等待 task1、task2 都完成
}
// viewModelScope 取消时,所有子协程一起取消

3.3 coroutineScope 与结构化并发

// coroutineScope:挂起直到所有子协程完成,任一子协程失败会取消整个作用域
suspend fun loadData() = coroutineScope {
    val user = async { fetchUser() }
    val posts = async { fetchPosts() }
    // 挂起,等待 user 和 posts 都完成
    Pair(user.await(), posts.await())
}

3.4 取消是协作式的

// cancel() 只是设置取消标志,协程需主动响应
viewModelScope.launch {
    while (isActive) {  // 检查取消状态
        doWork()
    }
}

// 挂起函数(delay、withContext 等)会自动检查取消
viewModelScope.launch {
    delay(5000)  // 若协程被取消,会抛出 CancellationException
}

// 计算密集型任务需手动检查
viewModelScope.launch {
    for (i in 0 until 1_000_000) {
        if (!isActive) return@launch
        heavyComputation(i)
    }
}

3.5 viewModelScope 与 lifecycleScope

// ViewModel 中:ViewModel 销毁时自动取消
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            val data = repository.fetchData()
            _liveData.value = data
        }
        // Activity 销毁 → ViewModel.onCleared() → viewModelScope.cancel()
    }
}

// Activity/Fragment 中:生命周期 DESTROYED 时自动取消
class MineActivity : AppCompatActivity() {
    private fun refreshAfterDelay() {
        refreshJob = lifecycleScope.launch {
            delay(500)
            refreshData()
        }
        // Activity 销毁 → lifecycleScope.cancel() → refreshJob 取消
    }
}

3.6 资源清理

viewModelScope.launch {
    try {
        val stream = openFile()
        use(stream) { /* 使用 */ }
    } finally {
        // 取消时 finally 仍会执行
        releaseResource()
    }
}

四、Android 代码示例汇总

4.1 ViewModel 中使用 viewModelScope

class MainViewModel : BaseViewModel() {
    private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

    fun getHomeList(page: Int) {
        post<HomeListApi, HomeVideoEntity>(...)  // 使用回调式 API
    }

    // 若使用协程
    fun loadWithCoroutine() {
        viewModelScope.launch {
            val data = withContext(Dispatchers.IO) {
                repository.fetchData()
            }
            mHomeLiveData.value = data
        }
    }
}

4.2 Activity 中使用 lifecycleScope

// MineActivity 中的延迟刷新
private fun delayAndExecute(delayMillis: Long, action: () -> Unit) {
    refreshJob?.cancel()
    refreshJob = lifecycleScope.launch {
        delay(delayMillis)
        action.invoke()
    }
}

4.3 取消与清理

// 在 onDestroy 中取消,lifecycleScope 会自动处理
// 若需手动取消自定义 Job:
override fun onDestroy() {
    refreshJob?.cancel()
    super.onDestroy()
}

五、总结对照表

主题要点
协程原理无栈协程、CPS 转换、状态机、Continuation、挂起不阻塞线程
异常传播launch 向上传播、async 在 await 暴露、SupervisorJob 隔离
异常处理CoroutineExceptionHandler(根协程)、try-catch、CancellationException 需重新抛出
结构化并发父子关系、取消传播、协作式取消、coroutineScope 等待子协程
Android 作用域viewModelScope(ViewModel 销毁取消)、lifecycleScope(DESTROYED 取消)

参考:Kotlin 协程官方文档、Android 架构组件、kotlinx.coroutines 源码