我们来把Kotlin协程这件事从头到尾聊透,保证用最通俗的大白话来讲解,让你不仅学会怎么用,还能理解它背后的门道。
可以把协程想象成一个超级轻量级的、可以随时"摸鱼"的任务。线程是公司里的正式员工,虽然能干很多活,但招聘和解雇(创建和销毁)的成本很高。而协程就像是你手头的具体工作(比如写一份PPT),你可以随时暂停去干别的事(被挂起),回头再接着写,而你的工位(线程)在此期间被其他人使用着 。
1. 协程的三大核心支柱
Kotlin协程的设计围绕着三个紧密相关的概念:挂起函数、协程作用域、协程上下文。理解它们,就掌握了协程的基石。
1.1 挂起函数 (suspend fun):魔法发生的地方
这是协程最核心的魔力所在。
- 是什么? 一个被
suspend关键字标记的函数。它代表这个函数可能会耗时 ,并且可以在不阻塞当前线程的情况下暂停执行,等结果准备好了再恢复 。 - 有什么用? 让异步代码"同步化"。不用写一堆回调函数,代码逻辑看起来就像从上到下顺序执行一样。
- 怎么工作的(原理简析)? 编译器在背后把挂起函数变成了一个"状态机"。每次函数可能暂停时,都会有一个对应的状态。当函数恢复时,会根据状态标签跳到上次执行的地方继续执行 。
kotlin
suspend fun fetchUserData(): String {
println("开始请求网络") // 运行在某个线程
// withContext 是一个挂起函数,它会将协程切换到 IO 线程池
// 当前协程在此处被挂起,但外面的线程可以去干别的活
val result = withContext(Dispatchers.IO) {
delay(1000) // 模拟网络请求,这是一个挂起函数
return@withContext "User Data"
}
println("网络请求结束,拿到数据:$result") // 恢复执行,可能回到了原来的线程
return result
}
1.2 协程作用域 (CoroutineScope):协程的"管辖区域"
- 是什么? 可以理解为一个协程的管理员 。它定义了所有通过它启动的协程的生命周期边界。它最重要的职责是保证结构化并发 。
- 结构化并发的好处:
- 取消传播:如果取消一个作用域,它内部所有正在运行的协程都会被取消。
- 等待完成:一个作用域会等待它内部所有的子协程都完成,自己才算完成。
- 异常传播:如果子协程出错了,它会通知其他兄弟协程或父协程。
- 常见的作用域:
GlobalScope:全局大喇叭 。生命周期与整个应用程序一样长,非常容易导致协程泄漏,几乎不推荐在实战中使用 。runBlocking:笨重的桥梁 。它会阻塞当前线程 直到内部所有协程执行完毕。通常只用在main函数测试或连接既有阻塞代码,正式业务代码中禁止使用 。coroutineScope:智能的挂起者 。它是一个挂起函数,会创建一个新的作用域,但不会阻塞线程,而是挂起自己,直到内部所有子协程完成。非常适合在挂起函数内进行并行分解任务 。- Android专属 :
viewModelScope(在ViewModel中) 和lifecycleScope(在Activity/Fragment中)。它们会自动绑定生命周期,当页面销毁时,自动取消所有协程,安全无泄漏 。
1.3 协程上下文 (CoroutineContext):协程的"背包"
- 是什么? 一个类似于
Map的数据结构,里面装着协程运行所需的各种配置信息 。这个背包里主要有四大件:Job(工作证) :协程的唯一标识和控制器。通过Job,你可以管理协程的生命周期 :启动、取消、判断是否活跃等 。launch返回一个Job对象。async返回一个Deferred对象(Deferred继承自Job,多了一个await()方法用于获取结果) 。
CoroutineDispatcher(调度器) :决定这个协程在哪条线程或哪个线程池上执行 。Dispatchers.Main:主线程,用于UI操作。Dispatchers.IO:IO线程池,用于网络、数据库、文件读写。Dispatchers.Default:默认线程池,用于CPU密集型任务,如JSON解析、排序。Dispatchers.Unconfined:不限派,在第一挂起点前运行在当前线程,之后由恢复它的线程决定,慎用 。
CoroutineName(名字):给协程起个名,方便调试 。CoroutineExceptionHandler(异常处理器):处理未捕获的异常 。
2. 如何启动协程:两大核心构建器
你需要在一个 CoroutineScope 里,使用构建器来启动协程。
-
launch:点火起飞,不要结果- 当你只关心执行任务,而不需要任务返回任何值时使用。它返回一个
Job对象,你可以用它来取消协程或等待它完成 。
- 当你只关心执行任务,而不需要任务返回任何值时使用。它返回一个
-
async:点火起飞,等待结果- 当你需要执行一个任务,并且最终要拿到它的返回值时使用。它返回一个
Deferred对象,通过调用它的await()方法来获取结果。async常用于并发执行多个独立任务 。
- 当你需要执行一个任务,并且最终要拿到它的返回值时使用。它返回一个
kotlin
// 假设这是在一个 ViewModel 或 lifecycleScope 里
viewModelScope.launch {
// 启动一个不返回结果的协程
val job = launch(Dispatchers.IO) {
// 执行一些IO操作,比如写入日志
}
// 并发执行两个网络请求
val userDeferred = async { fetchUser() } // 自动继承上下文,通常在默认调度器
val postsDeferred = async { fetchPosts() }
// 在这里挂起,等待两个结果都准备好
val user = userDeferred.await()
val posts = postsDeferred.await()
// 更新UI (此时已在主线程)
showUserData(user, posts)
}
3. 协程的"花式操作"
3.1 并发与等待
join():等待由launch启动的Job完成 。await():等待async启动的Deferred的结果 。- 组合并发 :利用
async实现并发,再通过await()汇聚结果。注意 :不要在async后直接跟await(),那样就变回串行了 。
3.2 协程的取消
取消是协作式的。意思是,协程代码必须自己配合检查是否被取消,否则无法被取消 。
- 如何让协程可以被取消?
- 调用
yield()或ensureActive()来检查状态 。 - 检查
isActive这个布尔值 。 - 使用所有
kotlinx.coroutines包下的挂起函数(如delay、withContext等),它们都是可取消的。
- 调用
- 释放资源 :在
finally块中释放资源。但如果finally中又调用了挂起函数,需要用withContext(NonCancellable) { ... }包裹 。
3.3 异常处理
- 传播方式 :
launch将异常直接抛出 给父协程;async将异常存储在Deferred对象里 ,直到await()调用时才抛出 。 CoroutineExceptionHandler:可以设置全局的"异常兜底方案",但通常作用域内的结构化并发已经能很好地处理异常传播 。supervisorScope:主管作用域 。它与coroutineScope类似,但最大的区别是:一个子协程失败了,不会影响其他兄弟子协程。非常适合处理多个互相独立的任务 。
4. 协程的高级数据流
4.1 热数据通道:Channel
- 是什么? 类似于一个阻塞队列 。用于不同协程之间传递数据(Stream)。一个协程发送,一个协程接收 。
4.2 冷数据流:Flow
- 是什么? 可以理解为异步版本的
Sequence(序列) 。它是一个冷流,即只有当你开始收集 (collect) 它的时候,里面的代码才会执行并发射数据 。 - 有什么用? 非常适合处理从数据库或网络源源不断返回大量数据的场景,或者接收实时更新。
- 关键操作符 :
- 构建:
flow { ... }、flowOf、asFlow。 - 中间操作:
map、filter、catch(捕获上游异常)。 - 末端操作:
collect、toList、first。
- 构建:
4.3 状态持有者:StateFlow 与 SharedFlow
StateFlow:状态容器 。它总是保存一个最新的值,并且只有值发生变化时才发射。非常适合替代LiveData在ViewModel中持有UI状态 。SharedFlow:事件广播器。可以配置缓存策略,用于发送事件,比如一次性提示、导航事件等(但要注意避免事件丢失或重复消费)。
5. 总结与实战示例
让我们用一个完整的Android ViewModel 示例来串起所有知识:
kotlin
class MyViewModel : ViewModel() {
// 1. UI状态,使用 StateFlow 持有,对外暴露不可变的 StateFlow
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
fun loadUserProfile(userId: String) {
// 2. 在 viewModelScope 中启动协程,自动感知生命周期
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
// 3. 并发获取用户信息和好友列表
val userProfile = coroutineScope { // 使用 coroutineScope 进行结构化并发
val userDeferred = async(Dispatchers.IO) { userRepository.fetchUser(userId) }
val friendsDeferred = async(Dispatchers.IO) { friendRepository.fetchFriends(userId) }
// 4. 挂起等待两个结果
UserProfile(
user = userDeferred.await(),
friends = friendsDeferred.await()
)
}
// 5. 更新状态(自动在主线程)
_uiState.value = UiState.Success(userProfile)
} catch (e: Exception) {
// 6. 异常处理
_uiState.value = UiState.Error(e.message)
}
}
}
// 密封类表示不同的UI状态
sealed class UiState {
object Loading : UiState()
data class Success(val profile: UserProfile) : UiState()
data class Error(val message: String?) : UiState()
}
}
希望这份详解能帮你彻底打通Kotlin协程的任督二脉。从今天起,写异步代码,享受同步代码的丝滑吧!