理解 Kotlin 协程:入门指南
随着现代应用程序变得越来越复杂,处理异步操作和并发任务变得至关重要。从移动应用的后台数据加载到服务器端的请求处理,我们经常需要在不阻塞主线程(通常是 UI 线程)的情况下执行耗时操作。传统的处理方式,如回调函数、Future/Promise 或者复杂的线程管理,往往会导致代码变得难以阅读、维护和理解,俗称“回调地狱”或“线程噩梦”。
Kotlin 协程(Coroutines)正是为解决这一痛点而生。它提供了一种全新的、更简洁、更易于理解的方式来编写异步和并发代码。本文将作为一份入门指南,带你深入理解 Kotlin 协程的核心概念、优势以及如何开始使用它们。
1. 什么是协程?(Coroutines)
首先,让我们来理解协程到底是什么。简单来说,协程是一种比线程更轻量级的并发机制。
- 线程(Threads) 是操作系统级别的概念。每个线程都有自己的栈空间,由操作系统内核进行调度。创建和切换线程的开销相对较大。当一个线程阻塞(例如等待网络响应或文件读写)时,整个线程都会停止执行,直到阻塞解除。
- 协程(Coroutines) 则是用户空间的轻量级概念。协程的调度由开发者或协程库在用户空间完成,而不是由操作系统内核。一个线程可以运行成百上千甚至更多的协程。当一个协程“暂停”(Kotlin 协程的
suspend
特性)等待某个结果时,它会释放其占用的线程,允许同一线程上的其他协程运行。当等待的结果可用时,协程可以从暂停的地方恢复执行。这种“暂停”和“恢复”的能力是协程的核心特征。
可以把线程想象成一家工厂的生产线,一条线同一时间只能生产一种产品。而协程则像是生产线上的不同工人,他们可以在需要等待原材料(例如网络数据)时暂时停下手中的工作,让其他工人继续,等原材料到了再接着做,所有工人都在同一条生产线上灵活协作。
2. 为什么选择 Kotlin 协程?
Kotlin 协程并非唯一实现协程的语言或库,但它在 Kotlin 中具有一些独特的优势:
- 轻量级: 创建和运行大量协程比创建大量线程的开销小得多。这使得在需要处理大量并发任务时,协程是更高效的选择。
- 简化异步代码: Kotlin 协程允许你使用看起来像同步的、顺序的代码风格来编写异步逻辑。这极大地提高了代码的可读性和可维护性。告别层层嵌套的回调,让异步流程变得直观。
- 结构化并发 (Structured Concurrency): 这是一个非常重要的特性。协程可以组织成层级结构,一个父协程可以管理其子协程的生命周期。当父协程被取消时,其所有子协程也会被取消。这有助于防止资源泄露和未完成的任务,使得并发代码更健壮。
- 内置取消支持: 协程的设计天然支持取消。你可以方便地取消一个正在运行的协程,并且协程库提供了协作式的取消机制,使得在耗时操作中检查取消状态并及时停止成为可能。
- 易于与其他库集成: Kotlin 协程可以轻松地与现有的 Java 库(通过适配器)以及 Kotlin 生态系统中的库(如 Flow、Channel)结合使用。在 Android 开发中,它与 Jetpack 组件(ViewModelScope, LifecycleScope)深度集成。
3. 核心概念:suspend
函数
理解 Kotlin 协程的第一步是理解 suspend
关键字。
suspend
是一个修饰函数的关键字,它表示该函数是一个“可暂停”的函数。这到底意味着什么?
一个 suspend
函数可以:
- 调用另一个
suspend
函数。 - 执行常规的非阻塞计算。
- 执行一个阻塞操作(例如网络请求或文件读写),并在操作完成前暂停自身的执行,同时释放其所在的线程。
- 当阻塞操作完成后,协程可以恢复执行,从它暂停的地方继续。
关键点: suspend
函数不能从常规的非 suspend
函数直接调用。你只能在另一个 suspend
函数、协程构建器(Coroutine Builder,如 launch
或 async
)内部或协程作用域(Coroutine Scope)内调用 suspend
函数。
看一个简单的例子:
“`kotlin
// 一个模拟耗时操作的 suspend 函数
suspend fun fetchData(): String {
println(“开始获取数据…”)
// delay 是一个 suspend 函数,它会暂停当前协程的执行,但不阻塞线程
// 1000 毫秒后协程会在同一个或不同的线程上恢复
kotlinx.coroutines.delay(1000)
println(“数据获取完成”)
return “这是获取到的数据”
}
fun main() {
// 错误: suspend 函数 fetchData() 不能直接调用
// fetchData() // 这会编译错误
}
“`
为了调用 fetchData()
这样的 suspend
函数,我们需要在协程的上下文中使用它。这通常是通过协程构建器来完成。
4. 协程构建器(Coroutine Builders)
协程构建器是启动新协程的函数。最常用的两个是 launch
和 async
。它们都是在 CoroutineScope
上定义的扩展函数。
4.1 launch
: “发射并忘记”
launch
是一个协程构建器,它启动一个新的协程,并且不返回任何结果。它通常用于执行那些只需要完成任务而不需要返回值的异步操作,例如更新 UI、执行日志记录等。
launch
返回一个 Job
对象,代表了协程的生命周期(活跃、完成、取消等)。你可以使用 Job
对象来等待协程完成(job.join()
)或取消它(job.cancel()
)。
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking { // runBlocking 是一个特殊的构建器,用于阻塞当前线程直到其内部的协程完成
println(“主程序开始”)
// 使用 launch 启动一个新协程
val job = launch {
println("协程1开始执行")
// 调用 suspend 函数
delay(500)
println("协程1执行完毕")
}
// launch 不阻塞当前协程,所以这行会立即执行
println("协程1已启动,不等待")
// 等待协程1完成
job.join()
println("协程1已完成")
// 启动另一个协程,这个协程不会被等待
launch {
println("协程2开始执行")
delay(1000)
println("协程2执行完毕")
}
println("主程序结束 (协程2可能还在后台运行)")
// runBlocking 会等待所有子协程完成,所以协程2也会完成
}
“`
输出可能类似:
主程序开始
协程1开始执行
协程1已启动,不等待
协程1执行完毕
协程1已完成
协程2开始执行
协程2执行完毕
主程序结束 (协程2可能还在后台运行)
4.2 async
: 执行任务并期望结果
async
是另一个协程构建器,它也启动一个新的协程,但它会返回一个 Deferred<T>
对象。Deferred
是一个轻量级的非阻塞 Future,你可以使用它的 await()
方法来获取协程的执行结果。await()
是一个 suspend
函数,它会暂停当前协程的执行,直到 async
启动的协程完成并产生结果。
async
常用于需要并发执行多个任务并等待它们全部完成后才能继续的场景。
“`kotlin
import kotlinx.coroutines.*
suspend fun getData1(): String {
println(“获取数据1…”)
delay(800)
println(“数据1获取完成”)
return “Data 1”
}
suspend fun getData2(): String {
println(“获取数据2…”)
delay(1200)
println(“数据2获取完成”)
return “Data 2”
}
fun main() = runBlocking {
println(“主程序开始”)
val deferred1 = async { getData1() } // 异步启动数据获取1
val deferred2 = async { getData2() } // 异步启动数据获取2
println("两个数据获取任务已异步启动")
// await() 是 suspend 函数,会等待相应 async 任务完成并获取结果
val result1 = deferred1.await()
val result2 = deferred2.await()
println("所有数据获取完成")
println("结果1: $result1, 结果2: $result2")
println("主程序结束")
}
“`
输出可能类似:
主程序开始
获取数据1...
获取数据2...
两个数据获取任务已异步启动
数据1获取完成
数据2获取完成
所有数据获取完成
结果1: Data 1, 结果2: Data 2
主程序结束
在这个例子中,getData1()
和 getData2()
是并发执行的。总的执行时间大约是两个任务中最长的时间 (1200ms),而不是它们串行执行的总时间 (800ms + 1200ms = 2000ms)。
launch
vs async
总结:
launch
: 用于不需要返回结果的任务,返回Job
。async
: 用于需要返回结果的任务,返回Deferred
(一个带有await()
方法的Job
)。
5. CoroutineScope:协程的作用域和生命周期
CoroutineScope
定义了协程的生命周期。在一个 CoroutineScope
内启动的协程(使用 launch
或 async
)都会被关联到这个作用域。当作用域被取消时,所有在其内部启动的协程也会被自动取消。这是实现结构化并发的关键。
- 为什么需要作用域? 考虑在一个 Android Activity 中启动一个网络请求协程。如果用户在请求完成前离开了 Activity,你希望这个协程自动停止,以避免不必要的资源消耗和潜在的内存泄露(例如,协程完成后尝试更新一个已经销毁的 Activity 的 UI)。将协程绑定到 Activity 的生命周期作用域可以自动处理这种情况。
- 如何获取作用域?
- 许多框架(如 Android Jetpack 的 ViewModel 和 Lifecycle)提供了预定义的作用域 (
viewModelScope
,lifecycleScope
)。 - 你可以自己创建一个作用域,通常通过组合
Job
和CoroutineContext
来实现。例如:val scope = CoroutineScope(Job() + Dispatchers.Main)
。当你需要取消所有在这个作用域内启动的协程时,只需调用scope.cancel()
。 runBlocking
构建器提供了它自己的作用域。coroutineScope
和supervisorScope
是两种特殊的suspend
函数,它们也创建子作用域,用于更细粒度的结构化并发控制。GlobalScope
: 这是一个全局作用域,其生命周期与整个应用程序的生命周期一样长。不推荐在大多数情况下使用GlobalScope
,因为它启动的协程不会被结构化管理,难以追踪和取消,可能导致资源泄露。只在极少数情况下(如应用程序启动时需要运行一个独立的、长时间运行的任务)考虑使用,且要非常谨慎。
- 许多框架(如 Android Jetpack 的 ViewModel 和 Lifecycle)提供了预定义的作用域 (
示例:手动创建和取消作用域
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// 创建一个带有 Job 和 Dispatcher 的 CoroutineScope
val scope = CoroutineScope(Job() + Dispatchers.Default)
val job1 = scope.launch {
repeat(5) { i ->
println("协程1: $i")
delay(200)
}
}
val job2 = scope.launch {
repeat(5) { i ->
println("协程2: $i")
delay(300)
}
}
delay(500) // 等待一段时间
println("取消作用域")
scope.cancel() // 取消 scope 内的所有协程
delay(1000) // 观察协程是否停止
println("主程序结束")
}
“`
输出可能类似:
协程1: 0
协程2: 0
协程1: 1
协程2: 1
协程1: 2
取消作用域
主程序结束
可以看到,当 scope.cancel()
被调用后,协程1和协程2都停止了执行,尽管它们内部的 delay
调用还没完成预设的时间。
6. CoroutineContext:协程的上下文
CoroutineContext
是一个集合,包含了协程运行时所需的一系列元素。它定义了协程的特性,例如:
Job
: 控制协程的生命周期,处理父子关系和取消。Dispatcher
: 决定协程在哪个线程或线程池上执行。CoroutineName
: 协程的名字,用于调试。CoroutineExceptionHandler
: 处理协程中未捕获的异常。
你可以通过 +
运算符来组合不同的上下文元素。当通过 launch
或 async
启动一个协程时,新的协程的上下文会由其父协程的上下文与传递给构建器的上下文参数合并而成。
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// 获取当前协程的上下文
val context = coroutineContext
println(“当前协程上下文: $context”)
// 在一个不同的 Dispatcher 上启动协程,并指定名字
launch(Dispatchers.IO + CoroutineName("MyWorker")) {
println("当前协程上下文: $coroutineContext")
println("当前线程: ${Thread.currentThread().name}")
}
delay(100) // 等待新协程执行
println("主程序结束")
}
“`
输出可能类似:
当前协程上下文: [StandaloneCoroutine{Active}@...]
当前协程上下文: [CoroutineName(MyWorker), StandaloneCoroutine{Active}@...]
当前线程: DefaultDispatcher-worker-1 @MyWorker#2
主程序结束
从输出中可以看出,launch
启动的协程拥有了指定的 CoroutineName
和 Dispatchers.IO
(虽然在 JVM 上,Dispatchers.IO
可能使用默认的线程池,名字会是 DefaultDispatcher-worker-X
,但它确实运行在 IO 线程池)。
7. Dispatchers:协程的执行器
Dispatcher
决定了协程在哪里运行,或者说,使用哪个线程池来执行协程中的代码。Kotlin 协程提供了几种标准的 Dispatchers:
Dispatchers.Main
: (通常在 Android 或支持 UI 框架的 JVM 应用中使用) 用于在主线程或 UI 线程上运行协程。适合执行 UI 更新、处理 UI 事件等需要与 UI 交互的操作。在没有主线程的环境(如纯 JVM 控制台应用)中,Dispatchers.Main
可能不可用或等同于Dispatchers.Default
。Dispatchers.IO
: 专为执行阻塞 IO 操作而优化,如网络请求、文件读写、数据库访问等。它使用一个共享的、按需创建的线程池,线程数量没有固定上限,可以根据需要创建大量线程(但会有限制)。Dispatchers.Default
: 专为执行 CPU 密集型任务而优化,如排序、复杂的计算、图片处理等。它使用一个大小限制在 CPU 核心数(或更高一点)的共享线程池。Dispatchers.Unconfined
: (慎用) 在启动协程时,它会在调用者所在的线程上立即执行。但当协程挂起(例如调用delay
或其他suspend
函数)并在稍后恢复时,它会恢复在任意合适的线程上,具体取决于唤醒它的函数和执行器。它不限制协程执行的线程。适用于那些既不消耗 CPU 也不进行 IO 且不需要特定线程的协程(极少见)。通常不推荐在 UI 或 CPU 密集型操作中使用。
如何切换 Dispatcher?
可以使用 withContext
函数在协程内部切换 Dispatcher。withContext
是一个 suspend
函数,它会切换到指定的上下文(通常是不同的 Dispatcher),执行其 lambda 块中的代码,然后切回原来的上下文。这对于在一个协程中执行不同类型的操作非常有用(例如,在 Dispatchers.Main
协程中切换到 Dispatchers.IO
执行网络请求,然后再切回 Main
更新 UI)。
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// 当前协程在 main runBlocking 线程上运行
println("主协程在: ${Thread.currentThread().name}")
// 在 IO 调度器上执行网络模拟操作
val data = withContext(Dispatchers.IO) {
println("模拟网络请求在: ${Thread.currentThread().name}")
delay(1000) // 模拟网络延迟
"一些数据"
}
// withContext 块执行完毕后,自动切回原来的上下文(这里是 runBlocking 线程)
println("网络请求完成,回到: ${Thread.currentThread().name}")
println("获取到的数据: $data")
// 在 Default 调度器上执行 CPU 密集型模拟操作
val result = withContext(Dispatchers.Default) {
println("模拟CPU计算在: ${Thread.currentThread().name}")
delay(1000) // 模拟计算时间
data.length * 2
}
println("CPU计算完成,回到: ${Thread.currentThread().name}")
println("计算结果: $result")
println("主程序结束")
}
“`
输出可能类似:
主协程在: main @coroutine#1
模拟网络请求在: DefaultDispatcher-worker-1 @coroutine#1
网络请求完成,回到: main @coroutine#1
获取到的数据: 一些数据
模拟CPU计算在: DefaultDispatcher-worker-2 @coroutine#1
CPU计算完成,回到: main @coroutine#1
计算结果: 10
主程序结束
注意 withContext
内部的代码运行在指定的 Dispatcher 对应的线程上,而 withContext
外部的代码则在原 Dispatcher 对应的线程上运行。withContext
是实现协程在不同线程之间平滑切换的关键工具。
8. 结构化并发(Structured Concurrency)深入理解
结构化并发是 Kotlin 协程的一大亮点。它通过父子协程关系和 CoroutineScope
来管理协程的生命周期。
- 当你在一个
CoroutineScope
中使用launch
或async
启动协程时,这个新协程会成为启动它的那个协程(或作用域)的子协程。 - 父协程会等待所有子协程完成。如果父协程被取消,它会递归地取消所有子协程。
- 如果一个子协程因为异常失败,通常会导致父协程以及同级子协程被取消。(这个行为可以通过
supervisorScope
修改)
这就像一个公司里的组织架构:部门经理(父协程)负责管理部门里的员工(子协程)。如果部门被解散(父协程取消),所有员工的工作(子协程)都会停止。如果一个员工犯了严重错误导致部门无法继续运作(子协程失败导致父协程取消),整个部门的工作都会停止。
这种结构的好处在于:
- 生命周期管理: 协程的生命周期与 UI 组件、业务流程等绑定,避免泄露。
- 错误传播: 异常会按照结构传播,更容易定位问题。
- 取消传播: 取消一个任务会取消其所有相关的子任务。
示例:结构化并发与取消
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking { // runBlocking 提供了作用域
val parentJob = launch { // launch 在 runBlocking 作用域内,parentJob 代表这个协程
println(“父协程启动”)
val childJob1 = launch { // 这个协程是 parentJob 的子协程
println("子协程1启动")
try {
delay(2000)
println("子协程1完成") // 如果在取消前执行,这行会输出
} finally {
// 在协程取消时执行清理操作
println("子协程1被取消或完成,清理资源")
}
}
val childJob2 = launch { // 这个协程也是 parentJob 的子协程
println("子协程2启动")
delay(1000) // 只延迟 1 秒
println("子协程2完成") // 这个会执行,因为它比父协程取消早完成
}
// 等待一段时间,让子协程启动并执行一部分
delay(500)
println("取消父协程")
parentJob.cancel() // 取消父协程
println("等待所有子协程结束...")
parentJob.join() // 等待父协程(以及其所有子协程)完成或被取消
println("父协程已结束")
}
// runBlocking 会等待 parentJob 完成
parentJob.join()
println("主程序结束")
}
“`
输出可能类似:
父协程启动
子协程1启动
子协程2启动
取消父协程
等待所有子协程结束...
子协程2完成
子协程1被取消或完成,清理资源
父协程已结束
主程序结束
可以看到,尽管子协程1计划运行2秒,但父协程在500ms时被取消,子协程1也随之被取消,并在 finally
块中执行了清理。子协程2因为在父协程取消前就完成了,所以正常打印了完成信息。
9. 协程的取消
协程的取消是协作式的。这意味着一个正在执行的协程只有在检查了取消状态后才会真正停止。大多数 kotlinx.coroutines
提供的 suspend
函数(如 delay
, withContext
, await
, join
, channel 操作等)都是可取消的。当它们被调用时,如果协程被取消,它们会抛出 CancellationException
。
如果你在编写自己的 suspend
函数,尤其是包含耗时计算循环的函数,需要定期检查协程的取消状态,例如使用 isActive
属性或 ensureActive()
函数。
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 100_000 && isActive) { // 检查 isActive
// 模拟一些工作
if (i % 1000 == 0) {
println(“工作中… $i”)
}
// 如果不检查 isActive,这个循环会一直运行直到完成,即使外部取消了
i++
}
println(“工作完成或被取消: $i”)
}
delay(100) // 等待一段时间
println("外部取消协程")
job.cancel() // 请求取消
job.join() // 等待协程完全结束(取消后清理完成)
println("主程序结束")
}
“`
输出可能类似:
工作中... 0
工作中... 1000
外部取消协程
工作中... 2000
工作完成或被取消: 2001 // 具体数值取决于取消时i的值
主程序结束
如果没有 && isActive
条件,while
循环会一直跑到 100000,尽管外部调用了 cancel()
。
10. 协程的异常处理
协程中的异常处理是一个相对复杂的话题,因为它与结构化并发和构建器类型有关。
-
launch
构建器:- 如果在一个
launch
启动的协程中发生未捕获的异常,该异常会向上传播到父协程。 - 如果父协程是使用
CoroutineScope
创建的(非supervisorScope
),并且它有一个Job
,那么父协程会因此被取消,并且异常会进一步上传,直到遇到一个CoroutineExceptionHandler
或runBlocking
的顶层。 - 可以使用
CoroutineExceptionHandler
来捕获和处理那些未被子协程自己处理的异常。将其添加到协程的CoroutineContext
中即可。
- 如果在一个
-
async
构建器:async
启动的协程中的异常会在调用await()
时被抛出。- 如果在调用
await()
之前发生异常,异常会存储在Deferred
对象中,直到await()
被调用时再抛出。 - 这意味着
async
不会将异常立即传播给父协程,只有在你尝试获取结果 (await()
) 时才会知道异常发生了。 - 因此,处理
async
异常的常用方式是在await()
调用外部使用try/catch
。
-
coroutineScope
vssupervisorScope
:coroutineScope
: 当其任何一个子协程失败时,会取消其自身和所有其他子协程,并将异常向上抛出。这是默认的行为,符合结构化并发中父子协程协同工作的模型。supervisorScope
: 当其子协程失败时,不会取消自身和同级的其他子协程。异常只会在该失败的子协程内部传播(如果子协程没有CoroutineExceptionHandler
,异常会被忽略或需要手动处理)。supervisorScope
常用于 UI 场景,例如在一个列表中启动多个独立的任务,其中一个失败不应该影响其他任务。
示例:异常处理 with launch
和 CoroutineExceptionHandler
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { context, exception ->
println(“捕获到未处理的异常: $exception”)
}
val job = launch(handler) { // 将 handler 添加到协程上下文
println("协程启动,即将抛出异常")
throw IllegalStateException("这是个测试异常")
}
job.join() // 等待协程结束
println("主程序结束")
}
“`
输出:
协程启动,即将抛出异常
捕获到未处理的异常: java.lang.IllegalStateException: 这是个测试异常
主程序结束
示例:异常处理 with async
和 try/catch
“`kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
println(“主程序开始”)
val deferred = async {
println("Async任务开始,即将抛出异常")
delay(500)
throw ArithmeticException("计算出错了")
"结果" // unreachable
}
try {
println("等待 Async 结果...")
val result = deferred.await() // 异常在这里被抛出
println("Async 结果: $result")
} catch (e: Exception) {
println("捕获到 Async 异常: $e")
}
println("主程序结束")
}
“`
输出:
主程序开始
Async任务开始,即将抛出异常
等待 Async 结果...
捕获到 Async 异常: java.lang.ArithmeticException: 计算出错了
主程序结束
11. 实际应用场景举例 (Android 伪代码)
在 Android 开发中,协程通常用于以下场景:
- 网络请求: 在 IO Dispatcher 中执行网络调用,然后在 Main Dispatcher 中更新 UI。
- 数据库操作: 在 IO Dispatcher 中执行数据库读写。
- 文件操作: 在 IO Dispatcher 中执行文件读写。
- 耗时计算: 在 Default Dispatcher 中执行 CPU 密集型计算。
- UI 逻辑: 在 Main Dispatcher 中处理用户输入、更新界面状态。
一个模拟的 ViewModel 中的数据加载例子:
“`kotlin
// 这是一个伪代码示例,假设你有一个 ViewModel
// import androidx.lifecycle.ViewModel
// import androidx.lifecycle.viewModelScope // ViewModelScope 是一个 CoroutineScope
// import kotlinx.coroutines.Dispatchers
// import kotlinx.coroutines.launch
// import kotlinx.coroutines.withContext
class MyViewModel(/ … /) { // : ViewModel() {
// ViewModelScope 绑定到 ViewModel 的生命周期
// 当 ViewModel 被清除时,viewModelScope 会被取消,所有子协程都会停止
// private val viewModelScope = CoroutineScope(Job() + Dispatchers.Main) // 实际ViewModel会自动提供
fun loadData() {
// 在 viewModelScope 中启动协程,确保其生命周期与 ViewModel 绑定
// launch 默认使用 ViewModelScope 的 Dispatcher (通常是 Dispatchers.Main)
viewModelScope.launch {
// 显示加载状态 (在 Main 线程)
showLoading(true)
println("UI线程: 开始加载数据")
try {
// 切换到 IO Dispatcher 执行网络请求
val data = withContext(Dispatchers.IO) {
println("IO线程: 模拟网络请求中...")
// 假设这是一个 suspend 网络请求函数
// val result = apiService.fetchUserData()
kotlinx.coroutines.delay(2000) // 模拟网络延迟
println("IO线程: 网络请求完成")
"从网络获取到的用户数据" // 模拟返回结果
}
// 网络请求完成后,自动切回原来的 Dispatcher (Main)
println("UI线程: 网络请求成功,准备更新UI")
// 更新 UI (在 Main 线程)
updateUIWithData(data)
} catch (e: Exception) {
// 处理异常 (在 Main 线程,因为异常在 withContext 块外被捕获)
println("UI线程: 数据加载失败: ${e.message}")
showError(e.message ?: "未知错误")
} finally {
// 隐藏加载状态 (在 Main 线程)
println("UI线程: 加载过程结束")
showLoading(false)
}
}
}
private fun showLoading(isLoading: Boolean) { /* ... 更新 UI ... */ }
private fun updateUIWithData(data: String) { /* ... 更新 UI ... */ }
private fun showError(message: String) { /* ... 更新 UI ... */ }
// 当 ViewModel 被清除时,viewModelScope 会被取消,无需手动取消
// override fun onCleared() {
// super.onCleared()
// viewModelScope.cancel() // ViewModelScope 会自动处理
// }
}
// 模拟 UI 更新函数
fun showLoading(isLoading: Boolean) = println(“UI: ${if(isLoading) “显示” else “隐藏”}加载”)
fun updateUIWithData(data: String) = println(“UI: 更新数据: $data”)
fun showError(message: String) = println(“UI: 显示错误: $message”)
// 演示调用
fun main() = runBlocking { // runBlocking 模拟一个阻塞的主线程环境来运行这个例子
val viewModel = MyViewModel()
viewModel.loadData()
// 在实际应用中,runBlocking 不应该用在这里,它会阻塞主线程。
// 这里只是为了让协程有机会执行完成
delay(3000) // 等待加载过程模拟完成
println("演示结束")
}
“`
这个例子清晰地展示了如何在 viewModelScope
中使用 launch
启动任务,如何使用 withContext(Dispatchers.IO)
执行后台操作,以及操作完成后如何自动切回主线程 (Dispatchers.Main
) 更新 UI,同时利用 try/catch/finally
处理异常和清理状态。
12. 总结与展望
Kotlin 协程为我们提供了一种强大且优雅的方式来处理异步和并发编程。通过 suspend
函数,我们可以用顺序式的代码风格编写复杂的异步流程;通过协程构建器 (launch
, async
),我们可以方便地启动任务;通过 CoroutineScope
和 CoroutineContext
,我们可以有效地管理协程的生命周期、调度和异常处理,实现健壮的结构化并发。
作为入门,掌握 suspend
、launch
、async
、CoroutineScope
、CoroutineContext
和 Dispatchers
这几个核心概念至关重要。
协程的世界远不止于此。在掌握了基础后,你可以进一步学习:
coroutineScope
vssupervisorScope
的详细区别和用法。- Channels: 用于协程之间的通信。
- Flows: 用于处理异步数据流。
select
表达式: 在多个suspend
操作中选择第一个完成的。- 单元测试协程: 使用
runBlockingTest
(或其他测试工具)。
通过不断的实践和学习,你会发现 Kotlin 协程能够极大地简化你的异步编程工作,提升代码的质量和可维护性。现在,是时候开始在你自己的项目中使用 Kotlin 协程,体验它带来的便利了!