Kotlin 协程:suspend 函数与 CoroutineScope
在 Kotlin 协程的世界里,suspend 函数和 CoroutineScope 是两个最基础也最重要的概念。理解它们的本质和关系,是掌握协程的第一步。本文将从实际使用角度出发,用最直白的方式帮助你建立清晰认知。
一、suspend 函数:可暂停的代码单元
1.1 什么是 suspend 函数
suspend 是 Kotlin 提供的一个关键字,用它修饰的函数被称为「挂起函数」。挂起函数的特殊之处在于:它可以在执行过程中暂停自己,稍后再恢复执行。
普通函数一旦开始执行,就会一直运行到结束,中间不能中断。而挂起函数则可以在某个时间点「停下来」,等某个异步操作完成后再「继续往下走」。
suspend fun fetchUserData(userId: String): User {
// 这里可以暂停执行
val user = withContext(Dispatchers.IO) {
// 模拟网络请求,这个操作会阻塞当前线程
apiService.getUser(userId)
}
return user
}
上面的代码中,fetchUserData 被 suspend 修饰后,就可以在 withContext 调用处暂停。withContext 会切换到 IO 线程执行网络请求,等请求完成后再切回原来的线程继续执行。整个过程不会阻塞任何线程。
1.2 挂起函数的本质
suspend 函数并不神秘,它本质上是一个「带有状态机逻辑的普通函数」。编译器会将每个 suspend 函数转换成类似状态机的结构。
函数执行流程示意:
普通函数:开始 → 执行 → 结束(线性,无中断)
suspend 函数:开始 → 执行 → 遇到挂起点 → 暂停 → 条件满足 → 恢复 → 结束
当你调用一个 suspend 函数时,编译器会生成一个「Continuation」对象(即「续体」),它记录了函数暂停的位置和需要恢复执行的上下文。协程调度器根据这个续体,在合适的时机恢复函数的执行。
1.3 挂起函数的调用规则
suspend 函数有一个重要限制:它只能在协程内部或另一个 suspend 函数中调用。
// 正确写法:suspend 函数内部调用另一个 suspend 函数
suspend fun fetchUserAndPosts(userId: String): UserWithPosts {
val user = fetchUserData(userId) // 调用另一个 suspend 函数,允许
val posts = fetchUserPosts(userId) // 允许
return UserWithPosts(user, posts)
}
// 错误写法:普通函数中直接调用 suspend 函数
fun loadData() {
val data = fetchUserData("123") // ❌ 编译报错
}
这条规则的目的是确保挂起操作始终在协程上下文中进行。如果你需要在普通函数中启动协程,就需要用到下面要讲的 CoroutineScope。
二、CoroutineScope:协程的作用域
2.1 为什么需要 CoroutineScope
协程需要一个「容器」来管理它的生命周期。这个容器就是 CoroutineScope。你可以把它理解成「协程的管辖范围」——在哪个范围内启动的协程,就由哪个范围负责管理。
CoroutineScope 负责的事情包括:
- 启动协程:提供
launch和async等方法来创建协程 - 跟踪协程状态:知道当前有多少协程在运行
- 取消协程:当作用域被取消时,所有子协程也会被取消
- 错误处理:收集子协程中的异常
2.2 全局作用域与结构化并发
Kotlin 协程的一个核心设计理念是「结构化并发」。每个协程都必须属于某个作用域,而且父子协程之间有明确的关系:父协程会等待所有子协程完成;父协程取消时,子协程也会级联取消。
fun exampleScope() = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun main() {
val scope = exampleScope()
scope.launch {
println("任务1开始")
delay(1000)
println("任务1完成")
}
scope.launch {
println("任务2开始")
delay(500)
println("任务2完成")
}
// 主线程休眠等待
Thread.sleep(2000)
// 取消整个作用域
scope.cancel()
}
这段代码中,两个协程都运行在同一个 CoroutineScope 下。当调用 scope.cancel() 时,两个协程都会被取消。打印结果类似:
任务1开始
任务2开始
任务2完成
任务1完成
2.3 常见的 CoroutineScope 创建方式
在实际开发中,你不会经常从头创建 CoroutineScope,而是使用现成的作用域。以下是几种最常用的方式:
| 作用域类型 | 作用范围 | 典型使用场景 |
|---|---|---|
GlobalScope |
应用整个生命周期 | 顶层单例、与 Activity/Fragment 无关的后台任务 |
viewModelScope |
ViewModel 生命周期 | UI 层数据加载,ViewModel 销毁时自动取消 |
lifecycleScope |
Lifecycle 所有者生命周期 | Activity/Fragment 生命周期感知型协程 |
| 自定义 Scope | 开发者指定范围 | 业务逻辑模块,按需控制作用域边界 |
// 方式1:使用 GlobalScope(需谨慎)
GlobalScope.launch(Dispatchers.Main) {
// 这里的协程在整个应用生命周期内都有效
}
// 方式2:在 ViewModel 中使用 viewModelScope
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = fetchData()
_uiState.value = data
}
}
}
// 方式3:使用 lifecycleScope( LifecycleOwner 如 Fragment)
lifecycleScope.launch {
// 当 Fragment 被销毁时自动取消
}
三、suspend 与 CoroutineScope 的关系
3.1 协作模式
理解这两者关系的关键是:suspend 函数负责「暂停」,CoroutineScope 负责「调度和管理」。
协程就像一辆汽车。CoroutineScope 是驾驶舱——控制汽车何时启动、何时停止。suspend 函数是油门和刹车——控制汽车的加速和减速,必要时可以让汽车停下来等待。
fun demo() = CoroutineScope(Dispatchers.Main).launch {
// 这个 launch 创建了一个协程(Scope + Coroutine)
val user = fetchUserData("123") // 这里暂停协程
// 协程恢复后继续执行
updateUI(user)
}
// suspend 函数必须在协程(或另一个 suspend 函数)中被调用
suspend fun fetchUserData(userId: String): User {
return withContext(Dispatchers.IO) {
api.getUser(userId)
}
}
3.2 典型的协程使用流程
一个完整的协程使用流程通常是这样的:
- 获取或创建 CoroutineScope:确定协程的生命周期边界
- 在作用域内启动协程:使用
launch或async创建协程任务 - 在协程内部调用 suspend 函数:执行具体的异步操作
- 协程自动管理:调度器根据情况暂停/恢复协程
- 作用域取消:所有子协程按需清理
class UserRepository(private val api: UserApi) {
// 自定义作用域,与 Repository 生命周期绑定
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun fetchUser(userId: String, onResult: (User) -> Unit) {
scope.launch {
try {
val user = api.getUser(userId) // suspend 调用
onResult(user)
} catch (e: Exception) {
onResult(null)
}
}
}
fun cancelAll() {
scope.cancel()
}
}
四、实战示例
4.1 一个完整的协程场景
假设你要实现一个「搜索用户」功能:用户输入关键字后,调用两个接口获取用户信息和文章列表,然后在主线程更新 UI。
class SearchViewModel(private val repository: UserRepository) : ViewModel() {
private val _searchResult = MutableLiveData<SearchResult>()
val searchResult: LiveData<SearchResult> = _searchResult
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
fun search(keyword: String) {
viewModelScope.launch {
_isLoading.value = true
try {
// 两个请求并行执行,提高效率
val userDeferred = async { repository.getUser(keyword) }
val postsDeferred = async { repository.getPosts(keyword) }
val user = userDeferred.await()
val posts = postsDeferred.await()
_searchResult.value = SearchResult(user, posts)
} catch (e: Exception) {
_searchResult.value = null
} finally {
_isLoading.value = false
}
}
}
override fun onCleared() {
super.onCleared()
// ViewModel 销毁时,viewModelScope 会自动取消所有协程
}
}
// Repository 中的 suspend 函数定义
class UserRepository(private val api: UserApi) {
suspend fun getUser(keyword: String): User {
return withContext(Dispatchers.IO) {
api.searchUser(keyword)
}
}
suspend fun getPosts(keyword: String): List<Post> {
return withContext(Dispatchers.IO) {
api.searchPosts(keyword)
}
}
}
这个示例展示了几个关键点:
viewModelScope确保协程与 ViewModel 生命周期绑定suspend函数封装异步细节,内部使用withContext切换线程async实现并行请求,await等待结果try/catch/finally处理异常和资源清理
4.2 常见错误与避免方法
| 错误做法 | 正确做法 | 问题原因 |
|---|---|---|
在 suspend 函数中调用 scope.launch |
使用 coroutineScope {} 或直接返回结果 |
嵌套 launch 会创建额外协程,破坏结构化并发 |
使用 GlobalScope.launch 处理 UI 逻辑 |
使用 viewModelScope 或 lifecycleScope |
GlobalScope 无法自动取消,导致内存泄漏 |
在 suspend 函数中直接使用 Thread.sleep() |
使用 delay() 函数 |
delay() 会挂起协程而不阻塞线程,Thread.sleep() 会阻塞线程 |
// 错误示例:在 suspend 函数中启动新协程
suspend fun fetchData(): Data {
val scope = CoroutineScope(Dispatchers.IO).launch {
// 这会创建一个「孤儿协程」,无法被外部取消
api.getData()
}
return scope.await() // 这种模式有问题
}
// 正确示例:使用 coroutineScope 进行结构化并发
suspend fun fetchData(): Data = coroutineScope {
// 这个作用域会继承外部协程的上下文
api.getData() // 直接返回结果
}
五、核心要点回顾
suspend 函数是 Kotlin 协程的基础构件,它让异步代码可以用同步的方式编写。CoroutineScope 则提供了协程生命周期管理的容器,确保协程在正确的时机启动和取消。两者的配合使得 Kotlin 协程既能写出简洁的异步代码,又能保持对资源使用的完全控制。
掌握这两个概念后,你可以进一步学习 Flow(用于处理数据流)、Channel(用于协程间通信)等高级特性。但无论协程功能如何扩展,suspend 和 CoroutineScope 始终是理解一切的起点。

暂无评论,快来抢沙发吧!