文章目录

Kotlin 协程:suspend 函数与 CoroutineScope

发布于 2026-04-05 23:16:43 · 浏览 14 次 · 评论 0 条

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
}

上面的代码中,fetchUserDatasuspend 修饰后,就可以在 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 负责的事情包括:

  • 启动协程:提供 launchasync 等方法来创建协程
  • 跟踪协程状态:知道当前有多少协程在运行
  • 取消协程:当作用域被取消时,所有子协程也会被取消
  • 错误处理:收集子协程中的异常

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 典型的协程使用流程

一个完整的协程使用流程通常是这样的:

  1. 获取或创建 CoroutineScope:确定协程的生命周期边界
  2. 在作用域内启动协程:使用 launchasync 创建协程任务
  3. 在协程内部调用 suspend 函数:执行具体的异步操作
  4. 协程自动管理:调度器根据情况暂停/恢复协程
  5. 作用域取消:所有子协程按需清理
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 逻辑 使用 viewModelScopelifecycleScope 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(用于协程间通信)等高级特性。但无论协程功能如何扩展,suspendCoroutineScope 始终是理解一切的起点。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文