第3章:Kotlin协程入门:GlobalScope、runBlocking、suspend函数与Job

协程这东西,刚接触时确实有点绕。我记得第一次看协程代码,满脑子都是「这玩意儿到底怎么跑的?」。别急,咱们一步步来拆解。

说白了,协程就是一套线程框架。但它比线程轻量得多。你想想看,开一万个线程能把机器搞崩,但开一万个协程?小意思。

3.1 GlobalScope:全局作用域

GlobalScope 是协程的「全局大管家」。它的生命周期跟整个应用绑定。应用不退出,它就不结束。

⚠️ 注意: 我个人习惯在生产代码中尽量少用 GlobalScope。为什么?因为它太「野」了,不受控制。
// 不推荐这样写
GlobalScope.launch {
    delay(1000)
    println("我跑在全局作用域里")
}

// 更好的做法:用自定义作用域
class MyViewModel : ViewModel() {
    private val scope = CoroutineScope(Dispatchers.Main)
    
    fun doSomething() {
        scope.launch {
            // 安全地执行协程
        }
    }
}

我在项目中遇到过一个问题:用 GlobalScope 启动了一个网络请求,用户退出了页面,请求还在跑。结果回调时 UI 已经销毁了,直接崩溃。嗯,从那以后我再也不敢随便用 GlobalScope 了。

3.2 runBlocking:阻塞的桥梁

runBlocking 是协程世界和普通世界之间的「桥梁」。它会阻塞当前线程,直到协程执行完毕。

💡 核心理解: runBlocking 主要用于 main 函数或测试场景。千万别在 Android 的主线程里用它,否则你会得到一个 ANR。
fun main() {
    println("开始:${Thread.currentThread().name}")
    
    runBlocking {
        delay(1000)
        println("协程内:${Thread.currentThread().name}")
    }
    
    println("结束:${Thread.currentThread().name}")
}

// 输出:
// 开始:main
// 协程内:main
// 结束:main

你发现没?整个代码都在主线程执行,但中间有 1 秒的延迟。这就是 runBlocking 的「阻塞魔法」。

📌 实战建议: 写单元测试时,runBlocking 是你的好帮手。我曾经用它写过上百个协程测试,稳得很。

3.3 suspend函数:协程的「暂停键」

suspend 是协程最核心的关键字。加了 suspend 的函数,可以在不阻塞线程的情况下暂停执行。

为什么会这样?因为 suspend 函数背后有个「续体传递风格」(CPS)的编译魔法。编译器会把 suspend 函数改造成一个状态机。

// 普通函数
fun fetchUser(): User {
    // 这里会阻塞线程
    Thread.sleep(1000)
    return api.getUser()
}

// suspend 函数
suspend fun fetchUserSuspend(): User {
    // 这里不会阻塞线程
    delay(1000)
    return api.getUser()
}

我刚开始学的时候,总觉得 suspend 函数和普通函数没啥区别。直到我看了反编译后的字节码——好家伙,一个简单的 suspend 函数被拆成了好几个状态。

特性 普通函数 suspend函数
线程阻塞 会阻塞 不会阻塞
调用限制 任何地方 只能在协程或另一个suspend函数中
编译后 普通方法 状态机 + Continuation
⚠️ 避坑指南: 我曾经在 RecyclerView 的 Adapter 里直接调用了 suspend 函数,结果编译报错。记住:suspend 函数只能在协程作用域内调用。

3.4 Job:协程的「身份证」

每个协程启动后,都会返回一个 Job 对象。它就是协程的「身份证」,用来控制协程的生命周期。

val job: Job = GlobalScope.launch {
    delay(1000)
    println("任务执行中")
}

// 等待协程完成
job.join()

// 取消协程
job.cancel()

// 检查状态
println("是否活跃:${job.isActive}")
println("是否完成:${job.isCompleted}")
println("是否取消:${job.isCancelled}")

Job 有几个关键状态:New、Active、Completing、Completed、Cancelling、Cancelled。嗯,看着挺多,但实际开发中你只需要关心三个:isActive、isCompleted、isCancelled。

🎯 实战技巧: 我习惯在 ViewModel 中这样管理协程:
class MyViewModel : ViewModel() {
    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    
    fun loadData() {
        scope.launch {
            // 网络请求
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        job.cancel() // 页面销毁时取消所有协程
    }
}

3.5 综合实战:一个完整的例子

咱们把今天学的串起来,写个完整的例子:

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("主程序开始")
    
    // 启动一个协程
    val job = launch {
        println("协程1开始")
        delay(1000)
        println("协程1结束")
    }
    
    // 启动另一个协程
    val job2 = launch {
        println("协程2开始")
        delay(500)
        println("协程2结束")
    }
    
    // 等待两个协程完成
    job.join()
    job2.join()
    
    println("主程序结束")
}

// 输出:
// 主程序开始
// 协程1开始
// 协程2开始
// 协程2结束
// 协程1结束
// 主程序结束

你发现没?协程2虽然后启动,但因为它只延迟了500ms,所以先结束。这就是协程的「非阻塞并发」——两个任务在同一个线程里交替执行,但谁都不阻塞谁。

📌 总结一下:
  • GlobalScope:全局作用域,慎用
  • runBlocking:阻塞桥梁,用于main和测试
  • suspend:暂停函数,不阻塞线程
  • Job:协程控制器,管理生命周期
这四个概念搞懂了,协程入门就算完成了。下一章咱们聊聊协程的调度器和上下文,那才是真正好戏开始的地方。