Kotlin协程完全指南:从原理到架构的艺术

100 阅读14分钟

第1章:协程是什么?

1.1 基本定义

协程(Coroutine)是一种轻量级的并发原语,它允许在单线程内实现挂起和恢复,从而能够编写出看似顺序执行但实际上可以进行异步操作的非阻塞代码。

1.2 协程三大核心特性

特性一:同步风格,异步执行

  • 是什么:允许开发者用顺序书写、看似阻塞的代码,来表达异步、非阻塞的业务逻辑。
  • 为什么关键:这是协程最直观的开发体验革命。它从根本上解决了"回调地狱",让异步代码像同步代码一样易读、易写、易调试。

特性二:挂起与恢复

  • 是什么:这是实现"同步风格异步执行"的底层机制suspend函数在执行到耗时操作时,可以挂起当前协程(保存状态、暂停执行),释放底层线程去执行其他任务;待操作完成后再在合适线程恢复执行。
  • 为什么关键:这是协程高性能的基石。它实现了"用更少的线程做更多的事",线程资源在等待期间不被阻塞,得以充分利用,从而支撑极高并发。

特性三:结构化并发

  • 是什么:将协程的执行与一个CoroutineScope(作用域)绑定,并建立父子关系。父协程的生命周期控制所有子协程(如:取消父协程会自动取消所有子协程)。
  • 为什么关键:这是协程可靠性的保障。它将并发任务的组织和生命周期管理自动化、结构化,从根本上避免了资源泄漏和后台任务失控,是现代协程库最重要的设计原则。

1.3 为什么要使用协程?

现实问题与协程解决方案对比

传统并发痛点协程解决方案实际收益
1. 回调地狱:多层嵌套回调,代码难以维护同步式异步代码:顺序书写,逻辑清晰代码可读性提升300%+,Bug减少40%+
2. 线程泄漏:忘记关闭线程/线程池,内存泄漏结构化并发:生命周期自动管理内存泄漏减少70%+,稳定性显著提升
3. 资源浪费:线程阻塞等待,CPU空闲挂起代替阻塞:线程充分复用服务器并发能力提升5-10倍,硬件成本降低
4. 并发控制复杂:锁、同步机制易出错Channel/Flow通信:更安全的并发原语并发错误减少60%+,开发效率提升
5. 上下文切换开销大:线程切换消耗CPU资源用户态轻量级切换:切换开销降低99%性能提升显著,响应时间缩短

第2章:协程和线程的区别

2.1 核心对比

特性协程线程
调度方式由程序员/协程库控制由操作系统内核调度
上下文切换成本极低(用户态切换)较高(用户态↔内核态切换)
内存占用极小(KB级别)较大(MB级别,有固定栈)
创建数量成千上万(理论上无限制)有限(几百到几千)
阻塞行为挂起(suspend),不阻塞线程阻塞(block),线程停止执行
并发模型基于协作式多任务基于抢占式多任务
通信方式Channel、Flow、共享状态锁、信号量、消息队列
适用场景I/O密集型、高并发服务CPU密集型、低延迟计算

2.2 深度解析

2.2.1 性能对比

  • 线程切换成本:约1-10微秒(需要进入内核态)
  • 协程切换成本:约100纳秒(纯用户态操作)

2.2.2 实际应用场景

kotlin

// 协程适合的场景:高并发I/O
suspend fun handleHttpRequests() {
    repeat(10000) { // 同时处理1万个请求
        launch {
            val response = fetchData() // I/O操作,协程挂起不阻塞线程
            processResponse(response)
        }
    }
}

// 线程适合的场景:CPU密集型计算
fun performHeavyCalculation() {
    thread { // 创建新线程进行复杂计算
        val result = computeMatrix() // 长时间CPU计算
        onResultReady(result)
    }
}

第3章:依赖方式

以典型的 Android 项目为例,你需要在模块的 build.gradle.kts 文件中的 dependencies 部分添加依赖:

kotlin

dependencies {
    // 协程核心库
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    // Android平台支持库
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

版本选择:建议使用与你的 Kotlin 编译器版本兼容的协程库。通常可以在 Kotlin官方文档 或协程库的 GitHub Release页面 找到对应关系。上例中的 1.7.3 是一个较新的稳定版本。


第4章:什么是挂起函数?

4.1 基本概念

挂起函数是由 suspend 关键字修饰的函数,它具备在执行过程中被暂停(挂起)  并在适当时候恢复执行的能力,同时不阻塞调用它的线程。

4.2 挂起函数的核心特性

  1. 可挂起:执行到耗时操作时暂停
  2. 可恢复:条件满足后继续执行
  3. 非阻塞:挂起期间线程可执行其他任务

4.3 执行流程示例

kotlin

// 示例:简单的挂起函数
suspend fun fetchData(): String {
    // 模拟网络请求
    delay(1000L) // 挂起点
    return "数据加载完成"
}

fun main() = runBlocking {
    println("开始请求数据...")
    val result = fetchData() // 看起来是同步调用
    println("结果:$result")
}

4.4 线程切换示例

图片描述

kotlin

suspend fun getUserInfo(): String {
    // 在主线程执行
    log("开始获取用户信息")
    
    // 切换到IO线程执行耗时操作
    val userData = withContext(Dispatchers.IO) {
        // 在IO线程执行网络请求
        api.getUserInfo()
    }
    
    // 自动切回主线程
    log("用户信息获取完成:$userData")
    return userData
}

挂起与恢复过程

  • 主线程 → IO线程:挂起
  • IO线程 → 主线程:恢复
  • 一行代码完成两个线程切换

第5章:suspend本质

5.1 CPS(Continuation-Passing Style)转换

5.1.1 CPS转换原理

suspend 的本质是编译器通过 CPS(Continuation-Passing Style)转换,将挂起函数转化为携带 Continuation 参数的状态机,实现非阻塞的挂起与恢复机制。

5.1.2 转换示例

kotlin

// 原始代码
suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "Tom"
}

// CPS转换后的函数签名
fun getUserInfo(continuation: Continuation<String>): Any?

5.1.3 Continuation接口

kotlin

interface Continuation<in T> {
    val context: CoroutineContext  // 协程上下文
    fun resumeWith(result: Result<T>)  // 恢复函数
}

5.2 状态机生成

5.2.1 状态机原理

编译器将函数体转换成一个状态机,将函数分割成多个部分,每个部分对应一个状态(label)。

5.2.2 状态机示例

kotlin

// 转换前(开发者视角)
suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "Tom"
}

// 转换后(编译器视角)
fun getUserInfo(continuation: Continuation<String>): Any? {
    val stateMachine = GetUserInfoStateMachine(continuation)
    val result = stateMachine.invoke()
    return result  // 可能返回 COROUTINE_SUSPENDED 或 "Tom"
}

// 状态机内部
class GetUserInfoStateMachine {
    var label = 0  // 0=初始状态,1=恢复状态
    
    fun invoke(): Any? {
        return when (label) {
            0 -> { /* 状态0:调用 withContext */ }
            1 -> { /* 状态1:返回 "Tom" */ }
        }
    }
}

5.3 CPS与状态机的关系

分工明确

组件作用类比
CPS改变函数调用方式,添加回调参数改变游戏规则:从"立即回答"变成"稍后回调"
状态机管理执行流程,保存和恢复状态游戏存档系统:记录玩到哪了,有什么道具
Continuation回调接口,连接挂起和恢复电话铃声:响了就知道外卖到了

协同工作流程

  1. CPS 提供框架:定义如何挂起(返回 COROUTINE_SUSPENDED)和如何恢复(通过 Continuation
  2. 状态机管理细节:记住执行到哪里了(label),保存中间数据
  3. 编译器自动转换:将同步代码转为异步状态机

第6章:CoroutineScope(协程作用域)

6.1 CoroutineScope的核心职责

CoroutineScope 的核心职责是管理协程的生命周期,它通过建立结构化的并发边界,确保所有协程任务有序、可控。当您取消一个 CoroutineScope 时,它会自动取消其内部所有子协程,如同一位高效的项目经理,及时终止所有下属任务,从而避免资源浪费和内存泄漏。

四大核心作用:

  1. 生命周期管理(Lifecycle Management)
    CoroutineScope 与特定生命周期绑定。当作用域因生命周期结束而被取消时,其内部所有协程都会被自动清理。
  2. 结构化并发(Structured Concurrency)
    CoroutineScope 建立了协程之间清晰的父子关系。取消父作用域会级联取消所有子协程。
  3. 异常传播(Exception Propagation)
    协程中未捕获的异常会沿着父子层次向上传播,可被作用域中设置的 CoroutineExceptionHandler 捕获。
  4. 提供协程上下文(Providing CoroutineContext)
    每个 CoroutineScope 都包含一个 CoroutineContext,定义了协程运行的环境。

6.2 各类作用域详解

6.2.1 GlobalScope(全局作用域)

定义:Kotlin标准库提供的单例全局协程作用域,生命周期与应用程序进程完全绑定

核心特点

  1. 进程级生命周期:内部协程除非手动取消,否则会持续存活直至应用结束。
  2. 破坏结构化并发:在此启动的协程是"孤儿协程",没有父Job。
  3. 极易导致内存泄漏:若协程持有短生命周期对象的引用,会阻止垃圾回收。

代码示例

kotlin

// ❌ 危险示例:在Activity中使用(典型内存泄漏)
class LeakyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GlobalScope.launch {
            delay(10000) // 模拟长时间任务
            updateUI() // 潜在崩溃点!
        }
    }
}

// ✅ 正确替代:使用lifecycleScope
class SafeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            delay(10000)
            if (isActive) { // 检查作用域是否仍活跃
                updateUI() // 安全执行
            }
        }
    }
}

6.2.2 runBlocking(阻塞构建器)

定义:一个协程构建器函数,用于在普通阻塞代码中启动协程,其核心行为是阻塞当前线程

使用场景

  • 程序入口点:Kotlin应用的main函数
  • 单元测试:在JUnit等测试框架中测试挂起函数

代码示例

kotlin

// ✅ 在main函数中的正确使用
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")
    }
    println("Hello,")
    // 输出: Hello, (等待1秒) World!
}

// ❌ 在Android中的致命错误用法
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        runBlocking { // 这会使UI线程完全卡死5秒!
            delay(5000L)
            initView() // 导致ANR
        }
    }
}

6.2.3 结构化作用域构建器

coroutineScope

定义:创建一个新的协程作用域,挂起调用者,直到其内部所有子协程执行完毕

特性一败俱败 - 任一子协程失败,会立即取消所有其他子协程。

代码示例

kotlin

suspend fun fetchConcurrentData(): CombinedData = coroutineScope {
    val data1Deferred = async { api.fetchDataA() }
    val data2Deferred = async { api.fetchDataB() }
    
    // 如果 fetchDataA 失败,fetchDataB 会被自动取消
    val data1 = data1Deferred.await()
    val data2 = data2Deferred.await()
    CombinedData(data1, data2)
}
supervisorScope

定义:创建一个使用 SupervisorJob 的协程作用域。

特性异常隔离 - 子协程失败互不影响。

代码示例

kotlin

suspend fun notifyAllServices(event: Event) = supervisorScope {
    listOf(serviceA, serviceB, serviceC).forEach { service ->
        launch {
            try {
                service.notify(event)
            } catch (e: Exception) {
                // 单个服务通知失败不影响其他
                log("Failed to notify ${service.name}")
            }
        }
    }
    // 等待所有通知尝试完成
}

6.2.4 Android 标准作用域

lifecycleScope

绑定对象ActivityFragment 等 LifecycleOwner

生命周期:在 onDestroy 时自动取消

代码示例

kotlin

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launchWhenStarted {
            // 只在Fragment处于STARTED或更活跃状态时执行
            loadAndDisplayData()
        }
    }
}
viewModelScope

绑定对象ViewModel

生命周期:在 ViewModel.onCleared() 时自动取消

核心优势跨越配置变更(如屏幕旋转),协程不会中断。

代码示例

kotlin

class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _userState = MutableStateFlow<UserState>(UserState.Loading)
    val userState: StateFlow<UserState> = _userState
    
    fun loadUser(userId: String) {
        viewModelScope.launch {
            _userState.value = UserState.Loading
            try {
                val user = repository.fetchUser(userId)
                _userState.value = UserState.Success(user)
            } catch (e: Exception) {
                _userState.value = UserState.Error(e.message)
            }
        }
    }
}

6.2.5 自定义作用域

创建方式val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

使用场景

  1. 非Android标准组件(如自定义的PresenterRepository、后台服务)
  2. 需要统一管理一组特定后台任务

代码示例

kotlin

class DataProcessor {
    // 创建自定义作用域
    private val processingScope = CoroutineScope(
        SupervisorJob() + Dispatchers.Default + CoroutineName("DataProcessor")
    )
    
    fun processBatch(items: List<DataItem>) {
        processingScope.launch {
            items.forEach { item ->
                launch {
                    processItem(item) // 每个项目独立处理
                }
            }
        }
    }
    
    // 必须提供的清理方法
    fun shutdown() {
        processingScope.cancel()
    }
}

6.3 总结与快速选择指南

作用域生命周期结构化并发异常传播典型使用场景
GlobalScope应用进程❌ 不支持需手动处理强烈不推荐,应避免
runBlocking阻塞线程至完成✅ 支持默认传播main函数、单元测试
coroutineScope挂起至子协程完成✅ 支持失败则全部取消原子性并行任务组
supervisorScope挂起至子协程完成✅ 支持失败互不影响独立的并行任务组
lifecycleScope绑定UI组件✅ 支持默认传播Activity/Fragment中的UI任务
viewModelScope绑定ViewModel✅ 支持默认传播ViewModel中的业务逻辑
自定义Scope手动控制✅ 支持取决于Job类型自定义生命周期组件

黄金法则

  1. 在Android中,永远优先使用 lifecycleScope 或 viewModelScope
  2. 需要并行执行多个任务时,根据任务关系选择 coroutineScope(整体)或 supervisorScope(独立)
  3. 永远不要在主线程使用 runBlocking,也几乎永远不要使用 GlobalScope
  4. 自定义作用域一定要记得在适当时机调用 cancel()

第7章:构建器

7.1 核心构建器对比总览

维度launchasyncrunBlocking
核心目的执行异步任务("即发即忘")计算异步结果("未来值")桥接阻塞代码与协程世界
返回值Job(任务句柄)Deferred<T>(未来结果)T(代码块结果)
线程行为非阻塞,立即返回非阻塞await()时挂起阻塞当前线程直到完成
典型场景UI事件、后台日志、触发操作并行网络请求、聚合数据、复杂计算程序main入口、单元测试、遗留代码迁移
使用警示需通过Job管理生命周期避免LAZY模式误用导致的顺序执行严禁在UI/主线程使用,谨防ANR

选择的黄金法则

  • 执行一个动作 → 用 launch
  • 获取一个结果 → 用 async
  • 在普通函数里调协程 → 用 runBlocking(仅限main、测试等特定场景)

7.2 launch:异步任务的执行者

核心概念

你可以把 launch 想象成对一个后台线程说:"去把这个事情做了,做完告诉我一声,但不用把具体东西带回来。" 它关注的是任务的执行过程本身

函数签名

kotlin

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

典型使用模式

kotlin

// 模式1:在ViewModel中启动后台任务
class MyViewModel : ViewModel() {
    fun refreshData() {
        viewModelScope.launch {
            _state.value = State.Loading
            try {
                val data = repository.fetchFromNetwork()
                _state.value = State.Success(data)
            } catch (e: Exception) {
                _state.value = State.Error(e.message)
            }
        }
    }
}

// 模式2:在Fragment/Activity中处理UI相关异步
class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launch {
            // 延迟执行UI操作
            delay(300)
            binding.progressBar.isVisible = false
        }
    }
}

7.3 async:异步结果的计算者

核心概念

如果把 launch 比作派活,async 就像是派一个带着任务清单和汇报要求的专员出去:"去把这些数据算一下,然后把结果报告带回来。" 它关注的是任务的产出结果

与launch的核心区别

特性launchasync
目的执行任务计算并返回结果
返回值Job(用于控制)Deferred<T>  (用于获取结果T
获取结果不支持deferred.await()  (挂起函数)
并发优势可并发执行多个任务并行执行多个任务,总耗时≈最慢任务

正确并发模式

kotlin

// ✅ 正确:真正的并行(总耗时约4秒)
suspend fun fetchDashboardData(): DashboardData = coroutineScope {
    // 1. 同时启动所有异步任务
    val userDeferred = async { api.getUserProfile() }    // 假设耗时3秒
    val postsDeferred = async { api.getUserPosts() }     // 假设耗时4秒
    val newsDeferred = async { api.getLatestNews() }     // 假设耗时2秒

    // 2. 统一等待所有结果
    // 总耗时 ≈ max(3, 4, 2) ≈ 4秒
    DashboardData(
        userDeferred.await(),
        postsDeferred.await(),
        newsDeferred.await()
    )
}

// ❌ 错误:顺序执行(总耗时约9秒)
suspend fun fetchSequentially(): DashboardData {
    // 注意:这里没有用coroutineScope
    val user = async { api.getUserProfile() }.await() // 等3秒
    val posts = async { api.getUserPosts() }.await()   // 再等4秒
    val news = async { api.getLatestNews() }.await()   // 再等2秒
    return DashboardData(user, posts, news) // 总耗时9秒!
}

高级模式:awaitAll 与结构化并发

kotlin

// 使用awaitAll处理任务集合
suspend fun batchProcessImages(urls: List<String>): List<Bitmap> = coroutineScope {
    val deferredList = urls.map { url ->
        async(Dispatchers.IO) { downloadImage(url) }
    }
    deferredList.awaitAll() // 等待所有下载完成
}

// 在supervisorScope内容忍单个失败
suspend fun robustBatchFetch(ids: List<String>): List<Data> = supervisorScope {
    ids.map { id ->
        async {
            try {
                fetchData(id)
            } catch (e: Exception) {
                null // 单个失败返回null,不影响其他
            }
        }
    }.awaitAll().filterNotNull() // 过滤掉失败项
}

7.4 runBlocking:阻塞世界的桥梁

核心概念与严重警告

runBlocking 会阻塞当前线程,直到它内部的协程全部执行完毕。这既是它的功能,也是最大的风险。

⚠️ 绝对禁止在UI线程(如Android主线程)使用 runBlocking,否则必然导致界面卡死(ANR)!

唯一正确的使用场景

  1. 程序入口点:Kotlin命令行应用的 main 函数
  2. 单元测试:在JUnit测试中测试挂起函数
  3. 极少数遗留代码迁移:作为临时过渡方案(应尽快重构)

代码示例

kotlin

// ✅ 场景1:main函数
fun main() = runBlocking {
    println("Hello,")
    launch {
        delay(1000)
        println("Kotlin!")
    }
    println("World")
    // 输出: Hello, World, (1秒后) Kotlin!
}

// ✅ 场景2:单元测试
@Test
fun `test user data loading`() = runBlocking {
    val user = repository.loadUser("123")
    assertEquals("John", user.name)
}

// ❌ 场景3:Android主线程(绝对错误!)
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        runBlocking { // 这将阻塞UI线程!
            delay(3000) // 用户会看到3秒白屏/卡死
            initView()
        }
    }
}

替代方案

在绝大多数需要异步执行的场景,请使用 lifecycleScope.launch 或 viewModelScope.launch,它们是非阻塞的,并且自动绑定生命周期。

7.5 Job:协程生命周期的遥控器

Job 对象是 launch 构建器的返回值,它代表了协程任务本身,是你管理其生命周期的控制中心。

核心职责

  • 状态查询:协程是活跃、完成还是取消了?
  • 生命周期控制:取消一个正在运行的任务
  • 等待同步:等待一个任务完成
  • 结构化关系:作为父Job,管理其子Job

状态机与核心属性

deepseek_mermaid_20251208_5d67f7.png

关键API实战

kotlin

val job = viewModelScope.launch {
    // 一些工作...
}

// 1. 取消任务
fun onUserCancel() {
    job.cancel() // 简单取消
}

// 2. 等待任务完成(非阻塞式挂起)
suspend fun waitForJob() {
    job.join() // 挂起当前协程,直到job完成
    println("任务已完成")
}

// 3. 组合操作:取消并等待清理
suspend fun gracefulShutdown() {
    job.cancelAndJoin() // 先请求取消,然后等待它完全结束
}

// 4. 状态检查
if (job.isActive) {
    println("任务仍在运行")
}

// 5. 完成回调(用于资源清理)
job.invokeOnCompletion { cause ->
    when {
        cause == null -> println("任务正常结束")
        cause is CancellationException -> println("任务被取消: ${cause.message}")
        else -> println("任务异常失败: $cause")
    }
}

实际应用:可取消的任务管理器

kotlin

class DownloadManager {
    private val downloadJobs = mutableMapOf<String, Job>()

    fun startDownload(fileId: String) {
        val downloadJob = viewModelScope.launch(Dispatchers.IO) {
            downloadFile(fileId)
        }
        downloadJobs[fileId] = downloadJob

        downloadJob.invokeOnCompletion {
            downloadJobs.remove(fileId) // 下载结束,清理引用
        }
    }

    fun cancelDownload(fileId: String) {
        downloadJobs[fileId]?.cancel("用户取消下载")
    }
}

7.6 Deferred:带结果的Job

Deferred 是 async 构建器的返回值,你可以把它理解为一个装有未来结果的盒子,它继承自 Job,所以拥有Job的所有控制能力,并额外提供了打开盒子(获取结果)的方法。

核心概念:继承关系

text

      Job(控制生命周期)
        ↑
   Deferred<T>(控制生命周期 + 获取结果T

核心API:await()

kotlin

val deferredResult: Deferred<String> = async {
    delay(1000)
    "计算结果"
}

// 获取结果(关键操作)
suspend fun getResult() {
    val result = deferredResult.await() // 挂起,直到结果就绪
    println("结果: $result")
}

异常处理

kotlin

val riskyDeferred = async {
    if (Random.nextBoolean()) "成功" else throw Exception("失败")
}

// 方式1:try-catch (最常用)
try {
    val result = riskyDeferred.await()
} catch (e: Exception) {
    // 处理异常
}

// 方式2:使用getCompletionExceptionOrNull检查
riskyDeferred.invokeOnCompletion { cause ->
    val exception = riskyDeferred.getCompletionExceptionOrNull()
    if (exception != null) {
        println("任务异常: $exception")
    }
}

进阶模式

kotlin

// 模式1:竞速模式(获取最先完成的结果)
suspend fun <T> fetchFastest(vararg providers: Deferred<T>): T {
    return select { // select表达式
        providers.forEach { deferred ->
            deferred.onAwait { it } // 监听每一个deferred
        }
    }
}

// 模式2:带缓存的请求
class CachedFetcher {
    private val cache = mutableMapOf<String, Deferred<Data>>()

    suspend fun fetch(key: String): Data = coroutineScope {
        // 如果已有相同请求在进行,直接等待其结果
        cache[key]?.let { return@coroutineScope it.await() }

        // 否则发起新请求
        val newDeferred = async { api.fetchData(key) }
        cache[key] = newDeferred
        
        try {
            newDeferred.await()
        } finally {
            cache.remove(key) // 请求完成,清理缓存
        }
    }
}

7.7 CoroutineStart:协程启动的四种策略

CoroutineStart 是一个枚举类,它作为 launch 和 async 的参数,精细控制协程何时以及如何开始执行。

四种模式速览

模式核心行为取消敏感性典型场景
DEFAULT立即调度(默认)高(可能未执行就被取消)绝大多数常规异步任务
LAZY懒加载,需手动触发资源惰性初始化、条件触发任务
ATOMIC原子启动,保证至少执行一次必须执行的日志、关键清理
UNDISPATCHED立即在当前线程执行第一段代码性能优化,避免初始线程切换

详解与示例

1. DEFAULT(默认策略)

kotlin

launch(start = CoroutineStart.DEFAULT) { // 默认值,可省略
    println("我会被尽快调度执行")
}
2. LAZY(懒加载策略)

kotlin

val lazyJob = launch(start = CoroutineStart.LAZY) {
    println("只有调用start()或join()时我才会执行")
}
// ... 某些条件成立后
lazyJob.start()
3. ATOMIC(原子启动策略)

kotlin

val atomicJob = launch(start = CoroutineStart.ATOMIC) {
    println("即使被立即取消,这行也一定会打印")
}
atomicJob.cancel() // 取消请求,但上面的println已执行
4. UNDISPATCHED(非调度策略)

kotlin

println("当前线程: main")
launch(start = CoroutineStart.UNDISPATCHED) {
    println("立即在main线程执行这一行") // 仍在main线程
    delay(100) // 遇到第一个挂起点
    println("挂起恢复后可能切换线程") // 可能不在main线程了
}

第8章:CoroutineContext深度解析

8.1 核心概念:什么是CoroutineContext?

CoroutineContext(协程上下文)是 Kotlin 协程中一个至关重要但抽象的概念。你可以把它理解为每个协程运行时随身携带的一个"工具箱"或"运行环境配置文件"

定义

CoroutineContext 是一个接口,本质上是一个以 Key 为索引的、不可变的、异构的元素集合。它包含了协程执行所需的所有上下文信息。

设计哲学

  • 结构化:子协程默认继承父协程的上下文,并可以修改
  • 组合性:不同的上下文元素可以通过 + 操作符灵活组合
  • 不可变性:任何修改都会返回一个新的上下文实例,确保了线程安全和可预测性

8.2 四大核心构成要素

元素类型主要作用对应的 Key
Job控制协程的生命周期(取消、状态)Job
CoroutineDispatcher决定协程在哪个或哪些线程上执行ContinuationInterceptor
CoroutineName为协程设置名称,便于调试和日志CoroutineName
CoroutineExceptionHandler处理协程中未捕获的异常CoroutineExceptionHandler

8.2.1 Job:生命周期的管理者

  • 每个协程都有一个 Job,它是协程生命周期和结构化并发的核心
  • 父协程的 Job 是其所有子协程的父级,取消父 Job 会递归取消所有子 Job

8.2.2 CoroutineDispatcher:协程调度器

调度器用途典型场景
Dispatchers.DefaultCPU 密集型任务。使用共享的线程池,其大小默认为 CPU 核心数。数据计算、排序、复杂逻辑处理
Dispatchers.IOI/O 密集型任务。使用专为阻塞式 I/O 操作优化的共享线程池。网络请求、数据库操作、文件读写
Dispatchers.Main主线程/UI 线程。平台相关,需引入对应 UI 库。更新 UI、调用 Android SDK 的 UI 方法
Dispatchers.Unconfined不限制任何特定线程。在调用者线程启动,在第一个挂起点后,由恢复它的挂起函数决定线程。慎用,主要用于某些特殊测试或高级场景
自定义 Executor将现有的 Executor(或 ExecutorService)转换为调度器。集成遗留线程池或需要精细控制线程资源时

代码示例

kotlin

// 在IO线程执行网络请求
launch(Dispatchers.IO) {
    val data = fetchFromNetwork()
    // 切换到主线程更新UI
    withContext(Dispatchers.Main) {
        updateUI(data)
    }
}

8.2.3 CoroutineName:协程的"身份证"

kotlin

launch(CoroutineName("网络请求协程")) {
    println("当前协程名: ${coroutineContext[CoroutineName]?.name}")
    fetchData()
}

8.2.4 CoroutineExceptionHandler:异常的最后防线

kotlin

val handler = CoroutineExceptionHandler { _, exception ->
    println("协程发生未捕获异常: $exception")
}

val scope = CoroutineScope(Job() + Dispatchers.Main + handler)
scope.launch {
    throw RuntimeException("测试异常") // 这个异常会被handler捕获
}

8.3 上下文的继承与组合规则

8.3.1 继承(结构化并发的基础)

当在一个协程作用域或另一个协程内部启动新协程时,子协程默认会继承父协程的上下文

kotlin

val parentScope = CoroutineScope(Dispatchers.Main + CoroutineName("父作用域"))

parentScope.launch {
    // 此协程继承:Dispatcher = Main, Name = "父作用域"
    println("父协程上下文: $coroutineContext")

    launch(Dispatchers.IO) {
        // 此子协程上下文:
        // - Job: 新的子Job(父Job是上一个协程的Job)
        // - Dispatcher: IO(显式指定,覆盖了继承的Main)
        // - Name: "父作用域"(从父协程继承,未被覆盖)
        println("子协程上下文: $coroutineContext")
    }
}

8.3.2 组合与覆盖("+"操作符)

上下文使用 + 操作符进行组合。关键规则是:右边的元素会覆盖左边具有相同 Key 的元素。

kotlin

// 定义一些上下文元素
val defaultDispatcher = Dispatchers.Default
val ioDispatcher = Dispatchers.IO
val errorHandler = CoroutineExceptionHandler { _, _ -> }
val parentJob = Job()
val debugName = CoroutineName("Debug")

// 组合上下文:右侧覆盖左侧相同Key的元素
val context1: CoroutineContext = defaultDispatcher + parentJob
val context2: CoroutineContext = ioDispatcher + errorHandler

val combined: CoroutineContext = context1 + context2 + debugName
// 最终的 combined 包含:
// - Dispatcher: IO (来自context2,覆盖了context1的Default)
// - Job: parentJob (来自context1,Key唯一)
// - CoroutineExceptionHandler: errorHandler (来自context2,Key唯一)
// - CoroutineName: "Debug" (来自debugName,Key唯一)

8.4 Android开发实战:ViewModel中的最佳实践

kotlin

class MyViewModel : ViewModel() {
    // 模拟 viewModelScope 的简化创建逻辑
    private val viewModelJob = SupervisorJob()
    
    // 组合上下文:Main调度器 + SupervisorJob + 可选的异常处理器
    private val viewModelContext: CoroutineContext = 
        Dispatchers.Main.immediate + viewModelJob + CoroutineExceptionHandler { _, throwable ->
            Log.e("MyViewModel", "Coroutine error", throwable)
        }
    
    // 创建作用域
    val viewModelScope = CoroutineScope(viewModelContext)
    
    override fun onCleared() {
        super.onCleared()
        // 当ViewModel销毁时,取消作用域内的所有协程
        viewModelJob.cancel()
    }
    
    fun fetchData() {
        viewModelScope.launch(CoroutineName("FetchUserData")) {
            // 默认在 Main 线程启动
            try {
                val data = withContext(Dispatchers.IO) {
                    // 切换到 IO 线程进行网络请求
                    repository.fetchData()
                }
                // 自动切回 Main 线程更新 LiveData/StateFlow
                _uiState.value = UiState.Success(data)
            } catch (e: Exception) {
                // 使用try-catch处理可恢复的业务异常
                _uiState.value = UiState.Error(e.message)
            }
        }
    }
}

8.5 总结:上下文的核心要义

  1. 环境决定行为CoroutineContext 定义了协程运行的"规则",从线程到异常处理

  2. 结构化是根本:继承机制确保了协程任务树的完整性,是自动取消和资源管理的基础

  3. 组合大于继承:通过 + 操作符,可以灵活地为不同层级的协程定制化上下文

  4. 明确职责范围

    • Job 管生命周期
    • Dispatcher 管线程
    • CoroutineName 管调试
    • CoroutineExceptionHandler 管未预料到的崩溃

第9章:withContext:安全的协程上下文切换器

9.1 核心概念:它是什么?

你可以将 withContext 理解为一个  "临时工作间"

  • 进入时:你带着任务(代码块)进入一个指定配置(如IO线程)的临时工作间
  • 工作时:你在这个工作间内完成任务
  • 离开时:你带着结果离开,并自动回到原来的工位(原来的线程)

9.2 函数签名与基本用法

kotlin

public suspend fun <T> withContext(
    context: CoroutineContext,        // 要切换到的目标上下文
    block: suspend CoroutineScope.() -> T // 要在新上下文中执行的挂起代码块
): T // 返回代码块的执行结果

最简单的例子:Android中的经典模式

kotlin

// 在ViewModel或Fragment中
lifecycleScope.launch { // 默认在UI线程启动
    // 1. UI线程:显示加载中...
    showLoading(true)
    
    // 2. 切换到IO线程执行耗时操作
    val data = withContext(Dispatchers.IO) {
        repository.fetchDataFromNetwork() // 网络请求
    }
    // 3. 自动切回UI线程
    showLoading(false)
    updateUI(data) // 安全更新UI
}

9.3 核心特性与对比

9.3.1 与 async 的关键区别

特性withContextasync { ... }.await()
目的临时切换上下文以执行代码,并返回结果启动一个新的并发子任务,并获取其未来结果
并发性顺序执行。挂起当前协程,执行完块内代码后才恢复可并行执行。多个async可以同时启动
返回值直接返回块内最后一行表达式的结果 (T)返回 Deferred<T>,需调用 await() 获取结果
性能场景总耗时 = 块内代码耗时 + 切换开销总耗时 ≈ 最慢的async任务耗时(当并行时)
典型用途"在后台线程干活,回主线程更新""同时发起多个独立网络请求,等所有结果"

选择的简单法则

  • 需要顺序执行一段代码,但这段代码需要在不同线程上运行 → 用 withContext
  • 需要同时发起多个独立任务,最后合并结果 → 用多个 async

9.3.2 与 launch + 线程切换的区别

旧模式 (繁琐) vs 新模式 (简洁)

kotlin

// ❌ 旧模式:使用 launch 和回调(或 Channel)
fun fetchDataOld(callback: (Result) -> Unit) {
    lifecycleScope.launch(Dispatchers.IO) {
        val result = repository.fetchData()
        withContext(Dispatchers.Main) { // 还需要切回来
            callback(result)
        }
    }
}

// ✅ 新模式:使用 withContext(线性逻辑)
suspend fun fetchDataNew(): Result {
    return withContext(Dispatchers.IO) { // 一行代码完成切换和返回
        repository.fetchData()
    }
    // 调用处可以在主线程直接使用结果更新UI
}

9.4 主要应用场景

场景1:安全地进行"后台工作,前台更新"

kotlin

suspend fun loadUserProfile(userId: String): UserProfileUiState {
    // 部分操作在IO线程
    val rawUserData = withContext(Dispatchers.IO) {
        userDao.getUserById(userId) ?: api.fetchUser(userId)
    }
    // 部分操作在Default线程(CPU计算)
    val processedData = withContext(Dispatchers.Default) {
        rawUserData.processStatistics() // 假设是CPU密集型计算
    }
    // 最终在调用者上下文(通常是Main)返回结果
    return UserProfileUiState(processedData)
}

场景2:组合多个调度器,优化执行流程

kotlin

suspend fun complexTask(input: Input): Output {
    // 阶段1: IO操作 (从多个源读取)
    val (data1, data2) = withContext(Dispatchers.IO) {
        val d1 = readFromFile(input.filePath)
        val d2 = queryDatabase(input.query)
        Pair(d1, d2)
    }
    
    // 阶段2: CPU计算 (处理数据)
    val processed = withContext(Dispatchers.Default) {
        heavyComputation(data1, data2)
    }
    
    // 阶段3: IO操作 (写入结果)
    withContext(Dispatchers.IO) {
        writeToFile(processed, "output.txt")
    }
    
    // 返回最终结果 (自动回到调用者线程)
    return processed.toOutput()
}

场景3:临时修改其他上下文元素

kotlin

suspend fun performTaskWithCustomContext() {
    val result = withContext(CoroutineName("NetworkTask") + Dispatchers.IO) {
        println("执行网络任务的协程名: ${coroutineContext[CoroutineName]?.name}")
        performNetworkCall()
    }
    // 离开 withContext 块后,协程名称恢复原样
}

场景4:实现超时和取消的包装

kotlin

suspend fun fetchDataWithTimeout(timeoutMs: Long): Data {
    return withContext(Dispatchers.IO) {
        try {
            // 为这个IO操作单独设置超时
            withTimeout(timeoutMs) {
                api.fetchData()
            }
        } catch (e: TimeoutCancellationException) {
            throw FetchTimeoutException("数据获取超时", e)
        }
    }
}

9.5 错误处理

kotlin

suspend fun safeDataLoad(): Result<Data> {
    return try {
        val data = withContext(Dispatchers.IO) {
            // 可能抛出 IOException 或网络异常
            api.fetchData()
        }
        Result.Success(data)
    } catch (e: IOException) {
        Result.Error(NetworkError(e))
    } catch (e: CancellationException) {
        throw e // 协程取消异常应重新抛出
    } catch (e: Exception) {
        Result.Error(UnexpectedError(e))
    }
}

9.6 性能考量与最佳实践

避免过度嵌套

kotlin

// ❌ 不易读
val x = withContext(A) {
    val y = withContext(B) {
        withContext(C) { computeY() }
    }
    process(x, y)
}

// ✅ 更清晰
suspend fun compute(): Result {
    val y = withContext(C) { computeY() }
    return withContext(A) { 
        val x = fetchX()
        process(x, y) 
    }
}

与 async 结合使用

kotlin

suspend fun fetchDashboardData(): DashboardData = coroutineScope {
    // 并行发起两个网络请求
    val userDeferred = async(Dispatchers.IO) { api.getUser() }
    val newsDeferred = async(Dispatchers.IO) { api.getNews() }
    
    // 等待结果,然后在主线程处理
    withContext(Dispatchers.Main) {
        val user = userDeferred.await()
        val news = newsDeferred.await()
        combineForUi(user, news) // UI组合逻辑
    }
}

9.7 总结:为什么 withContext 如此重要?

维度说明
安全性提供线程安全切换的黄金标准,尤其保障了UI操作必须在主线程执行
简洁性将异步回调代码  "拉直"  为线性顺序代码,极大提升了可读性和可维护性
组合性返回值的设计让它能轻松嵌入到其他挂起函数中,与 asyncflow 等完美结合
结构化继承外部作用域的上下文(如 Job),确保取消等信号能正确传递到内部代码块

一句话总结withContext 是 Kotlin 协程将  "做什么" (业务逻辑)与  "在哪里做" (执行线程/上下文)优雅解耦的关键工具,是编写清晰、健壮异步代码的基石之一。


第10章:协程取消深度解析

10.1 取消的本质:协作式而非抢占式

Kotlin协程的取消是"协作式"的。这意味着:

  • 外部可以随时请求取消一个协程(调用 job.cancel()
  • 内部的协程代码必须定期检查取消状态,并在适当的时候主动结束执行

10.2 如何发起取消

方法描述备注
job.cancel()请求取消该Job最常用的方法
job.cancel("原因")附带一个 CancellationException 消息利于调试
scope.cancel()取消整个作用域,内部所有Job都会被取消如 viewModelScope.cancel()
job.cancelAndJoin()先请求取消,然后挂起等待直到Job完全结束优雅停止,确保资源清理完毕

示例:基础取消

kotlin

val job = launch {
    repeat(1000) { i ->
        println("job: 我在数 $i ...")
        delay(500L)
    }
}

delay(1300L) // 延迟一段时间
println("main: 我等得有点不耐烦了...")
job.cancel() // 取消这个job
job.join()   // 等待job结束
println("main: 现在我可以继续了。")

输出:

text

job: 我在数 0 ...
job: 我在数 1 ...
job: 我在数 2 ...
main: 我等得有点不耐烦了...
main: 现在我可以继续了。

10.3 如何响应取消:检查取消状态

10.3.1 方式一:使用 isActive 扩展属性

kotlin

val job = launch(Dispatchers.Default) {
    var nextPrintTime = System.currentTimeMillis()
    var i = 0
    while (isActive) { // 使用 isActive 作为循环条件
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: 我在工作 ${i++} ...")
            nextPrintTime += 500L
        }
    }
    println("job: 我被取消了,但我在做最后的清理...")
}

10.3.2 方式二:使用 ensureActive() 函数

kotlin

val job = launch(Dispatchers.Default) {
    repeat(1000) { i ->
        ensureActive() // 检查点:如果已取消,直接抛异常
        println("job: 我在工作 $i ...")
        Thread.sleep(500L) // ❌ 注意:这是一个阻塞调用
    }
}

10.3.3 方式三:依赖其他挂起函数

kotlin

val job = launch(Dispatchers.Default) {
    var i = 0
    while (true) {
        if (i % 100 == 0) {
            yield() // 主动挂起并检查取消
        }
        i++
    }
}

检查方式对比表

方式优点缺点适用场景
isActive显式、灵活,可在循环结束后进行清理需要手动在循环条件或多个检查点判断需要在取消后执行额外逻辑的复杂循环
ensureActive()非常简洁,失败即抛异常,强制退出无法在取消后立即执行清理(异常会向上抛)简单的循环或操作,希望取消时立即停止
依赖挂起函数无需额外代码,由库函数自动处理依赖于是否调用了挂起函数任何包含标准库挂起函数的代码

10.4 异常处理:CancellationException

当一个协程因取消而退出时,退出的原因是 CancellationException

kotlin

val job = launch {
    try {
        repeat(1000) { i ->
            delay(500)
            println("打印 $i")
        }
    } catch (e: CancellationException) {
        println("协程被取消了,原因是: '${e.message}'")
        releaseResources() // 重要:清理资源
        throw e // 重新抛出,让协程框架知道它确实被取消了
    } finally {
        println("协程的 finally 块")
    }
}

10.5 资源清理:finally块与NonCancellable

kotlin

val job = launch {
    try {
        val resource = acquireResource()
        useResource(resource)
    } finally {
        println("正在释放资源...")
        
        // 使用 NonCancellable 上下文来执行必须完成的挂起清理操作
        withContext(NonCancellable) {
            delay(1000) // 这个挂起操作可以正常执行
            println("资源已异步释放完毕")
        }
    }
}

10.6 超时处理

10.6.1 withTimeout

kotlin

try {
    val result = withTimeout(1300L) {
        repeat(1000) { i ->
            println("我正在执行 $i ...")
            delay(500L)
        }
        "完成"
    }
    println("结果: $result")
} catch (e: TimeoutCancellationException) {
    println("操作超时了!")
}

10.6.2 withTimeoutOrNull

kotlin

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("我正在执行 $i ...")
        delay(500L)
    }
    "完成"
}
if (result == null) {
    println("操作超时,但没有异常。")
} else {
    println("结果: $result")
}

10.7 SupervisorJob与取消的特殊行为

SupervisorJob 与普通 Job 的核心区别在于处理子协程失败时的行为。简单来说,普通 Job 会因一个子协程的失败而“株连”整个作用域,而 SupervisorJob 则提供了隔离,允许其他子协程继续工作

为了方便你快速理解,我将它们的核心差异总结如下:

特性维度普通 Job (默认行为)SupervisorJob (监督行为)
异常传播子协程未捕获的异常会向上传播给父Job。子协程的异常不会向上传播给父Job或影响兄弟协程
取消传播父Job的取消会传播给所有子协程;一个子协程的失败也会导致父Job取消,进而取消所有其他子协程(“一死全死”)父Job的取消会传播给所有子协程;但一个子协程的失败不会导致父Job取消,因此兄弟协程不受影响
适用场景任务之间有依赖关系,需要同时成功或失败。例如,一组必须全部成功才算成功的并行计算。任务之间相互独立,希望单个任务失败不影响其他任务。例如,同时加载一个页面的多个独立模块数据

🔧 实现原理与使用关键

  1. 底层机制SupervisorJob 的特殊性源于其重写了 childCancelled() 方法,该方法直接返回 false。这告诉父Job:“这个子协程的取消/失败由我自己处理,你不用因此取消自己或通知你的其他孩子”。这是一种协作式取消

  2. 正确的创建方式:要使 SupervisorJob 生效,必须将其作为 CoroutineScope 的一部分或使用 supervisorScope 构建器。

    • 正确示例

      kotlin

      // 方式一:作为作用域的一部分
      val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
      scope.launch { /* 独立任务A */ }
      scope.launch { /* 独立任务B */ }
      
      // 方式二:使用 supervisorScope 构建器
      suspend fun work() = supervisorScope {
          launch { /* 独立任务A */ }
          launch { /* 独立任务B */ }
      }
      
    • 常见错误:将 SupervisorJob 作为参数传入 launch 或 async 是无效的,因为协程构建器会创建新的 Job 实例覆盖它

      kotlin

      // ❌ 错误:此处的 SupervisorJob() 不起作用,子协程失败仍会传播
      someScope.launch(SupervisorJob()) {
          launch { throw Exception() } // 仍会导致整个作用域取消
      }
      

⚠️ 重要注意事项与最佳实践

  • 异常仍需处理SupervisorJob 仅隔离了异常的传播,但未捕获的异常最终仍会导致应用崩溃(在Android上)或打印到控制台(在JVM上)。因此,在 supervisorScope 内,必须为每个可能抛出异常的子协程添加 try/catch,或使用 CoroutineExceptionHandler(注意:CoroutineExceptionHandler 仅在根协程上,即 supervisorScope 的直接子 launch 中有效)
  • 与 async 的配合:当在 supervisorScope 中使用 async 作为根协程(直接子协程)时,异常会被封装在 Deferred 对象中,只有在调用 await() 时才会抛出,不会立即崩溃。这为你提供了更灵活的异常处理时机。
  • 取消依然有效:虽然 SupervisorJob 不传播失败引起的取消,但手动调用作用域的 cancel() 或父协程的取消,仍然会正常传递给所有子协程。这是结构化并发的基础保证。

总结来说,SupervisorJob 的核心“特殊行为”是在子协程失败时充当了“防火墙” ,阻止了取消的连锁反应。它适用于管理一组独立并行任务的场景,让你可以更精细地控制程序的健壮性。

10.7.1 SupervisorJob的特殊性

kotlin

// 普通Job的行为:一个子协程失败会传播到父协程
val job = Job()
val scope = CoroutineScope(job + Dispatchers.Default)

scope.launch {
    delay(100)
    println("子协程1开始工作")
    throw RuntimeException("子协程1失败了!")
}

scope.launch {
    delay(200)
    println("子协程2开始工作") // 可能不会执行
}

10.7.2 SupervisorJob的取消传播特性

kotlin

val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + Dispatchers.Default)

scope.launch {
    delay(100)
    println("子协程1开始工作")
    throw RuntimeException("子协程1失败了!")
}

scope.launch {
    delay(200)
    println("子协程2正常执行") // 这个会正常执行
}

10.7.3 实际应用场景

kotlin

class ImageLoader {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    
    fun loadImage(url: String): Deferred<Bitmap> = scope.async {
        // 加载图片逻辑
    }
    
    fun cancelAll() {
        scope.cancel() // 仍然可以一次性取消所有任务
    }
}

10.8 Android中的取消最佳实践

1. 使用viewModelScope或lifecycleScope

kotlin

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = repository.loadData()
            _uiState.value = data
        }
    }
}

2. 在onCleared/onDestroy中取消自定义作用域

kotlin

class MyCustomManager {
    private val customScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    
    fun doHeavyWork() {
        customScope.launch { /* 长时间任务 */ }
    }
    
    fun cleanup() {
        customScope.cancel() // 必须手动取消!
    }
}

3. 结合SupervisorJob的使用场景

kotlin

class UserProfileViewModel : ViewModel() {
    fun loadUserData(userId: String) {
        viewModelScope.launch {
            supervisorScope {
                val userDeferred = async { userRepo.getUser(userId) }
                val postsDeferred = async { postRepo.getUserPosts(userId) }
                val friendsDeferred = async { friendRepo.getUserFriends(userId) }
                
                _userData.value = UserData(
                    user = userDeferred.awaitOrNull(),
                    posts = postsDeferred.awaitOrNull(),
                    friends = friendsDeferred.awaitOrNull()
                )
            }
        }
    }
}

第11章:协程异常捕获与处理

11.1 协程异常的传播机制

11.1.1 异常的自动传播

kotlin

val scope = CoroutineScope(Job() + Dispatchers.Default)

scope.launch {
    println("父协程开始")
    
    launch {
        println("子协程1开始")
        delay(100)
        throw RuntimeException("子协程1发生异常!")
    }
    
    launch {
        println("子协程2开始")
        delay(200)
        println("子协程2正常完成")
    }
    
    delay(300)
    println("父协程结束") // 这行不会执行
}

11.1.2 异常传播的可视化

text

普通launch协程的异常传播:
┌─────────────────────────────────────┐
│ 父协程 (launch)                      │
│  ┌─────────────────────────────────┐│
│  │ 子协程1 (抛出异常)               ││
│  │    → 异常向上传播 → 取消父协程   ││
│  └─────────────────────────────────┘│
│  ┌─────────────────────────────────┐│
│  │ 子协程2 (被取消)                ││
│  │    ← 父协程取消所有子协程       ││
│  └─────────────────────────────────┘│
└─────────────────────────────────────┘

11.2 协程异常处理的不同方式

11.2.1 方式一:try-catch直接捕获

kotlin

val scope = CoroutineScope(Dispatchers.Default)

scope.launch {
    try {
        throw IllegalArgumentException("参数错误")
    } catch (e: Exception) {
        println("捕获到异常: ${e.message}")
    }
}

11.2.2 方式二:使用CoroutineExceptionHandler

kotlin

val exceptionHandler = CoroutineExceptionHandler { context, exception ->
    println("CoroutineExceptionHandler捕获到异常: ${exception.message}")
}

val scope = CoroutineScope(Job() + Dispatchers.Default + exceptionHandler)

scope.launch {
    throw RuntimeException("测试异常")
}

11.2.3 方式三:supervisorScope内的异常隔离

kotlin

val scope = CoroutineScope(Dispatchers.Default)

scope.launch {
    supervisorScope {
        val job1 = launch {
            delay(100)
            throw RuntimeException("第一个任务失败")
        }
        
        val job2 = launch {
            delay(200)
            println("第二个任务正常完成")
        }
        
        joinAll(job1, job2)
    }
    println("supervisorScope结束后继续执行")
}

11.3 不同协程构建器的异常处理

11.3.1 launch vs async的异常处理差异

特性launchasync
异常抛出时机立即抛出并传播延迟到调用await()时抛出
异常处理位置在协程内部或父作用域在调用await()的地方
是否自动取消父协程是(除非使用SupervisorJob)是(除非使用SupervisorJob)

kotlin

// launch的异常处理
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
    throw RuntimeException("launch异常")
}
runBlocking { job1.join() }

// async的异常处理
val deferred = scope.async {
    throw RuntimeException("async异常")
}
runBlocking {
    try {
        deferred.await()
    } catch (e: Exception) {
        println("捕获async异常: ${e.message}")
    }
}

11.3.2 使用async的安全模式

kotlin

suspend fun <T> safeAsync(
    block: suspend () -> T
): Result<T> = coroutineScope {
    try {
        Result.success(block())
    } catch (e: CancellationException) {
        throw e
    } catch (e: Exception) {
        Result.failure(e)
    }
}

// 使用示例
scope.launch {
    val result1 = safeAsync { fetchUserData() }
    val result2 = safeAsync { fetchUserPosts() }
    
    when {
        result1.isFailure -> println("获取用户数据失败")
        result2.isFailure -> println("获取用户帖子失败")
        else -> {
            val user = result1.getOrNull()
            val posts = result2.getOrNull()
        }
    }
}

11.4 特定场景的异常处理

11.4.1 Android中的异常处理

kotlin

class MainActivity : AppCompatActivity() {
    private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        runOnUiThread {
            when (exception) {
                is NetworkException -> showNetworkError()
                is DatabaseException -> showDatabaseError()
                else -> showGenericError(exception)
            }
        }
        Crashlytics.logException(exception)
    }
    
    private val lifecycleScope = lifecycleScope + exceptionHandler
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            val result = try {
                repository.fetchData()
            } catch (e: Exception) {
                null
            }
            result?.let { updateUI(it) }
        }
    }
}

11.4.2 网络请求的异常处理

kotlin

suspend fun <T> safeApiCall(
    apiCall: suspend () -> T
): Result<T> {
    return try {
        Result.success(apiCall())
    } catch (e: IOException) {
        Result.failure(NetworkException("网络错误: ${e.message}", e))
    } catch (e: HttpException) {
        Result.failure(ServerException("服务器错误: ${e.code()}", e))
    } catch (e: Exception) {
        Result.failure(UnexpectedException("未知错误", e))
    }
}

11.4.3 Flow中的异常处理

kotlin

fun fetchDataFlow(): Flow<Result<Data>> = flow {
    emit(Result.success(Data("初始数据")))
    delay(1000)
    emit(Result.success(Data("更新数据")))
    throw IOException("网络断开")
}.catch { cause ->
    emit(Result.failure(NetworkException("获取数据失败", cause)))
}.onCompletion { cause ->
    cause?.let { println("Flow完成,有异常: ${it.message}") }
        ?: println("Flow正常完成")
}

11.5 异常处理的最佳实践

11.5.1 分层异常处理策略

kotlin

// 第一层:具体操作的异常处理
suspend fun performOperation(): Result<Data> {
    return try {
        val data = doComplexOperation()
        Result.success(data)
    } catch (e: SpecificBusinessException) {
        logBusinessException(e)
        Result.failure(e)
    }
}

// 第二层:领域层的异常处理
class DataUseCase {
    suspend fun execute(): Result<DomainResult> {
        return try {
            val result = performOperation()
            Result.success(validateAndTransform(result))
        } catch (e: ValidationException) {
            Result.failure(e)
        }
    }
}

// 第三层:表现层的异常处理
class ViewModel {
    fun loadData() {
        viewModelScope.launch {
            when (val result = useCase.execute()) {
                is Result.Success -> updateUI(result.data)
                is Result.Failure -> showError(result.exception)
            }
        }
    }
}

11.5.2 异常包装与分类

kotlin

sealed class AppException(
    message: String,
    cause: Throwable? = null
) : Exception(message, cause)

sealed class NetworkException(message: String, cause: Throwable? = null) : AppException(message, cause) {
    class NoInternetConnection(cause: Throwable? = null) : NetworkException("无网络连接", cause)
    class Timeout(cause: Throwable? = null) : NetworkException("请求超时", cause)
    class ServerError(val code: Int, message: String, cause: Throwable? = null) : NetworkException(message, cause)
}

11.5.3 测试中的异常处理

kotlin

class PaymentProcessor(
    private val paymentGateway: PaymentGateway,
    private val retryPolicy: RetryPolicy = ExponentialBackoffRetryPolicy()
) {
    suspend fun processPayment(amount: BigDecimal): PaymentResult {
        return retryPolicy.retry(
            operation = { paymentGateway.charge(amount) },
            shouldRetry = { e -> e is NetworkException || e is ServerException }
        ).fold(
            onSuccess = { receipt -> PaymentResult.Success(receipt) },
            onFailure = { e -> 
                when (e) {
                    is InsufficientFundsException -> PaymentResult.InsufficientFunds
                    is CardDeclinedException -> PaymentResult.CardDeclined(e.reason)
                    else -> PaymentResult.UnknownError(e.message)
                }
            }
        )
    }
}

11.6 异常处理常见陷阱

11.6.1 陷阱一:忽略CancellationException

kotlin

// ❌ 错误做法:吞掉CancellationException
try {
    delay(1000)
} catch (e: Exception) {
    println("捕获异常: ${e.message}")
}

// ✅ 正确做法:重新抛出CancellationException
try {
    delay(1000)
} catch (e: CancellationException) {
    logCancellation(e)
    throw e
} catch (e: Exception) {
    handleOtherException(e)
}

11.6.2 陷阱二:异常处理器使用不当

kotlin

// ❌ 错误做法:在子协程上设置异常处理器
val parentJob = Job()
val scope = CoroutineScope(parentJob + Dispatchers.Default)

scope.launch {
    launch(CoroutineExceptionHandler { _, e ->
        println("不会执行这里")
    }) {
        throw RuntimeException("异常")
    }
}

// ✅ 正确做法:在根协程或作用域级别设置异常处理器
val exceptionHandler = CoroutineExceptionHandler { _, e ->
    println("捕获到异常: ${e.message}")
}
val scope = CoroutineScope(Job() + Dispatchers.Default + exceptionHandler)

scope.launch {
    launch {
        throw RuntimeException("异常")
    }
}

11.7 监控与调试异常

11.7.1 添加异常监控点

kotlin

class MonitoredCoroutineExceptionHandler(
    private val crashReporting: CrashReporting,
    private val analytics: Analytics,
    private val defaultHandler: CoroutineExceptionHandler? = null
) : CoroutineExceptionHandler {
    
    override fun handleException(context: CoroutineContext, exception: Throwable) {
        val coroutineName = context[CoroutineName]?.name ?: "unnamed"
        val job = context[Job]
        
        crashReporting.report(
            exception = exception,
            metadata = mapOf(
                "coroutine_name" to coroutineName,
                "is_active" to (job?.isActive ?: false),
                "is_cancelled" to (job?.isCancelled ?: false)
            )
        )
        
        analytics.logEvent("coroutine_exception", mapOf(
            "type" to exception.javaClass.simpleName
        ))
        
        defaultHandler?.handleException(context, exception)
    }
}

11.7.2 调试协程异常的工具函数

kotlin

object CoroutineDebugger {
    inline fun <T> traceCoroutine(
        name: String,
        dispatcher: CoroutineDispatcher = Dispatchers.Default,
        crossinline block: suspend () -> T
    ): Deferred<T> {
        val scope = CoroutineScope(SupervisorJob() + dispatcher)
        
        return scope.async(CoroutineName(name)) {
            try {
                println("🏁 协程 '$name' 开始执行")
                val startTime = System.currentTimeMillis()
                val result = block()
                val duration = System.currentTimeMillis() - startTime
                println("✅ 协程 '$name' 成功完成,耗时 ${duration}ms")
                result
            } catch (e: CancellationException) {
                println("⏹️  协程 '$name' 被取消: ${e.message}")
                throw e
            } catch (e: Exception) {
                println("❌ 协程 '$name' 发生异常: ${e.message}")
                throw e
            }
        }
    }
}

11.8 总结

协程异常处理的关键要点:

  1. 理解异常传播机制:在结构化并发中,异常会自动向上传播(除非使用SupervisorJob)

  2. 选择合适的处理方式

    • 使用try-catch处理局部异常
    • 使用CoroutineExceptionHandler处理全局未捕获异常
    • 使用supervisorScope隔离异常传播
  3. 区分不同构建器的行为

    • launch:异常立即传播
    • async:异常延迟到await()时抛出
  4. 正确处理CancellationException:不要吞掉取消异常

  5. 建立分层的异常处理策略:从具体操作到全局处理器

  6. 测试异常处理逻辑:模拟各种异常场景

  7. 监控和调试:添加监控点跟踪异常