Kotlin协程浅入浅出

纯干货,不废话,开整!

协程与线程之间对比

特性 协程 线程
资源开销 轻量级(基于线程池复用) 重量级(内核线程,MB 级内存)
上下文切换 无系统调用,用户态完成 需要内核介入,开销大
并发模型 协作式(需手动挂起) 抢占式(由系统调度)
Android 主线程使用 通过 Dispatchers.Main 安全更新 UI 直接操作需 runOnUiThread

协程解决什么问题

相比于 CallbackAsyncTask这种历史出现过的异步任务处理方法,协程写法更加简洁。

  1. 使用同步写法,逻辑更加直观,避免主线程阻塞,防止 ANR。
  2. 解决回调地狱(Callback Hell)。
  3. 结构化并发(Structured Concurrency)自动绑定生命周期,防止内存泄漏。
  4. 简化并发任务协调,使用 async 启动任务,并在 await() 中获取结果。
  5. Jetpack 组件深度集成,有 Google 官方背书。

协程的优势是什么

  1. 代码简洁:用同步写法实现异步逻辑,告别回调嵌套。
  2. 生命周期安全:通过结构化并发自动管理资源,避免内存泄漏。
  3. 高效线程调度:智能切换线程(如 Dispatchers.Main 更新 UI)。
  4. 无缝整合生态:与 Retrofit、Room、LiveData 等 Jetpack 组件完美协作。
  5. 低开销:协程是用户态线程,切换成本远低于系统线程。

协程的底层实现原理

一句话概括,协程底层是基于挂起函数与状态机,结合线程调度和结构化并发机制实现的。

下面对这句话里面的核心概念进行解释和说明。

协程的核心:挂起函数与状态机

  • 挂起函数(Suspend Functions):
    • 通过 suspend 关键字标记的函数,可以在不阻塞当前线程的情况下暂停协程的执行。
    • 挂起的本质是保存当前协程的上下文(如局部变量、执行位置),并让出线程资源,允许其他任务运行。
  • 状态机(State Machine):
    • Kotlin 编译器会将挂起函数转换为一个状态机。每个挂起函数被编译为一个类,内部通过 label 跟踪执行状态。
    • 每次遇到挂起点(如 delay()withContext()),状态机的 label 会更新,并通过 Continuation 接口保存和恢复上下文。

通过查看编译期生成的伪代码,可以观察到其内部是通过 label 标识状态的状态机实现。

kotlin 复制代码
// 源码
suspend fun fetchData() {
    val data = networkRequest() // 挂起点 1
    process(data)              // 挂起点 2
}

// 编译器生成的伪代码(状态机形式):
class FetchDataStateMachine : Continuation<Unit> {
    var label = 0
    var data: Data? = null

    override fun resumeWith(result: Result<Unit>) {
        when (label) {
            0 -> {
                label = 1
                networkRequest(this) // 挂起,传递 Continuation
            }
            1 -> {
                data = result.getOrThrow()
                process(data)
                label = 2
            }
            // ...
        }
    }
}

协程调度与线程切换

  • 调度器(Dispatchers):
    • 协程通过 Dispatchers(如 MainIODefault)决定代码在哪个线程执行。
      • Dispatchers.Main:在 Android 主线程执行,底层通过 Handler.post()Looper 实现。
      • Dispatchers.IO:使用线程池处理阻塞 I/O 操作(如网络请求、文件读写)。
  • 线程切换:
    • 当协程通过 withContext(Dispatchers.IO) 切换调度器时,底层会提交任务到目标线程池,挂起当前协程,并在目标线程恢复执行。

结构化并发与协程作用域

  • 结构化并发(Structured Concurrency):
    • 协程通过 CoroutineScope(如 viewModelScope、lifecycleScope)管理生命周期。
    • 父协程取消时,所有子协程会自动取消,避免内存泄漏。
  • JobSupervisorJob
    • Job 用于跟踪协程状态,SupervisorJob 允许子协程独立失败而不影响父协程(如处理独立 UI 组件)。

协程的底层实现库:kotlinx.coroutines

  • 协程的启动:
    • launch 或 async 启动协程时,会创建一个 Coroutine 对象,并将其调度到指定线程。
  • 挂起与恢复:
    • 协程挂起时,通过 Continuation.resumeWith() 恢复执行,切换回目标调度器。

协程异常传输机制

协程需要运行在 CoutineScope 上下文中,而 CoutineScope 是具备层级关系的,一个父 Scope 可以具有多个子 Scope。

普通Job------异常向上传播,取消父协程及兄弟协程

在默认场景下(使用普通Job),对协程异常的处理流程描述如下:

  1. 当子协程抛出未捕获的异常时,异常会向上传播到父协程。
  2. 父协程会因此被取消,并进一步取消其所有其他子协程。
  3. 最终,异常会传递到根协程,如果没有被处理(例如通过CoroutineExceptionHandler),可能导致程序崩溃。

图:普通Job异常传播流程

示例代码如下:

kotlin 复制代码
runBlocking {
    val parentJob = launch {
        launch {
            delay(100)
            throw RuntimeException("子协程异常")
        }
        launch {
            delay(200)
            println("此协程可能不会执行") // 父协程被取消,此任务被中断
        }
    }
    parentJob.join()
}

// 输出:父协程因异常终止,第二个子协程未执行

SupervisorJob------将异常隔离控制在子协程本身

Supervisor:导师,指导教师

在使用SupervisorJob或SupervisorScope时,则会截断异常透传过程,将异常隔离在子协程本身:

  1. 子协程的异常不会影响父协程或其他兄弟协程。
  2. 父协程不会被取消,其他子协程可以继续执行。
  3. 但抛出异常的子协程自身会被取消,其异常需要通过其他方式处理(例如在async中结合await捕获)。

图:SupervisorJob异常传递流程

在开发Android应用时,使用的viewModelScope本身就是基于SupervisorJob,已经具备异常隔离功能。

示例代码如下:

kotlin 复制代码
runBlocking {
    val supervisor = SupervisorJob()
    launch(supervisor) {
        launch {
            delay(100)
            throw RuntimeException("子协程异常")
        }
        launch {
            delay(200)
            println("此协程正常执行") // 不受异常影响
        }
    }
    delay(300)
}
// 输出:第二个子协程正常执行

协程异常处理的最佳实践

协程的异常处理不同于线程,虽然知识点不复杂,但稍有不慎,就会造成异常逃逸、未按设想中的情况进行捕获的问题。

try-catch ------ 捕获同步和挂起函数中的异常

在协程内部直接使用try-catch块处理可能抛出异常的代码。

kotlin 复制代码
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        try {
            delay(100)
            throw RuntimeException("模拟异常")
        } catch (e: Exception) {
            println("捕获异常: ${e.message}")
        }
    }
    delay(200) // 等待协程执行
}
// 输出:捕获异常: 模拟异常

CoroutineExceptionHandler ------ 处理未捕获异常

为协程作用域添加全局异常处理器,处理未被try-catch捕获的异常。根据协程异常的传播机制,该处理器也可自动捕获到子协程异常

kotlin 复制代码
import kotlinx.coroutines.*

fun main() {
    val handler = CoroutineExceptionHandler { _, e ->
        println("全局捕获异常: ${e.message}")
    }
    val scope = CoroutineScope(Job() + handler)
    
    scope.launch {
        throw RuntimeException("未捕获的异常")
    }
    
    Thread.sleep(500) // 等待协程执行
}
// 输出:全局捕获异常: 未捕获的异常

SupervisorJob/supervisorScope ------ 隔离异常

子协程的异常不会影响父协程和其他子协程,这意味着子协程异常不会让父协程、兄弟协程取消,但未被捕获的异常仍然有可能导致应用崩溃。

kotlin 复制代码
import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    val scope = CoroutineScope(coroutineContext + supervisor)
    
    scope.launch {
        delay(100)
        throw RuntimeException("子协程1异常")
    }
    
    scope.launch {
        delay(200)
        println("子协程2正常执行")
    }
    
    delay(300)
}
// 输出:子协程2正常执行(但子协程1的异常未被处理,可能导致崩溃)

结合使用CoroutineExceptionHandler和SupervisorJob

在复杂场景中,全局处理器和异常隔离结合使用。

kotlin 复制代码
import kotlinx.coroutines.*

fun main() {
    val handler = CoroutineExceptionHandler { _, e ->
        println("全局捕获异常: ${e.message}")
    }
    val scope = CoroutineScope(SupervisorJob() + handler)
    
    scope.launch {
        throw RuntimeException("子协程1异常")
    }
    
    scope.launch {
        delay(200)
        println("子协程2正常执行")
    }
    
    Thread.sleep(500)
}
// 输出:
// 全局捕获异常: 子协程1异常
// 子协程2正常执行

处理async协程中的异常------在await()时进行捕获

使用async启动协程时,异常会在调用await()时抛出,需在此处捕获。

kotlin 复制代码
import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred = async {
        delay(100)
        throw RuntimeException("async任务异常")
    }
    
    try {
        deferred.await()
    } catch (e: Exception) {
        println("捕获async异常: ${e.message}")
    }
}
// 输出:捕获async异常: async任务异常

协程异常处理小结

  1. 作用域选择:
    • 使用SupervisorJob或supervisorScope隔离子协程异常。
    • viewModelScope和lifecycleScope已内置SupervisorJob。
  2. 异常捕获方式:
    • 同步代码:直接使用try-catch。
    • 全局异常:通过CoroutineExceptionHandler。
    • async任务:在await()时捕获异常。
  3. Android注意事项:
    • 避免在子协程中未处理异常导致应用崩溃。
    • 结合LiveData或StateFlow传递错误状态到UI层。
相关推荐
tangweiguo030519872 小时前
Android Kotlin AIDL 完整实现与优化指南
android·kotlin
alexhilton18 小时前
深入理解Jetpack Compose中的函数的执行顺序
android·kotlin·android jetpack
tangweiguo030519871 天前
Kotlin集合全解析:List和Map高频操作手册
kotlin
前行的小黑炭1 天前
Retrofit框架分析(二):注解、反射以及动态代理,Retrofit框架动态代理的源码分析
android·kotlin·retrofit
老码识土1 天前
Kotlin 协程源代码泛读:Continuation 思想实验-2
android·kotlin
人生游戏牛马NPC1号1 天前
学习Android(三)
android·kotlin
洛阳泰山1 天前
LangChain4j 搭配 Kotlin:以协程、流式交互赋能语言模型开发
java·ai·语言模型·kotlin·交互·springboot·langchain4j
LCY1332 天前
spring security +kotlin 实现oauth2.0 认证
java·spring·kotlin
0wioiw02 天前
Kotlin基础(①)
android·开发语言·kotlin