81、Kotlin协程入门:GlobalScope、launch、async、withContext、Dispatchers

协程这东西,刚接触时觉得玄乎,用久了会发现——它其实就是帮你把异步代码写得像同步一样自然。我最早在项目里引入协程时,团队里还有人质疑:「这不就是线程池换了个马甲吗?」后来大家真香了。

今天咱们就把协程的这几个核心概念掰开揉碎。你跟着我走一遍,保证能上手。

协程是什么?一句话说清楚

协程是一种轻量级的并发框架。轻量到什么程度?一个线程里可以跑成千上万个协程,而线程切换的开销几乎可以忽略。

说白了,协程就是帮你管理「什么时候该等,什么时候该继续」的工具。你不需要手动写回调,不需要担心线程切换,只需要告诉它:「我要做这件事,做完再干下一件。」

核心思想:协程让异步代码看起来像同步代码,但不会阻塞线程。

GlobalScope:全局作用域,慎用

GlobalScope 是协程的顶级作用域。它的生命周期跟整个应用一样长。什么意思?就是你启动了一个协程,如果不手动取消,它会一直运行到应用进程结束。

我在项目里见过有人图省事,到处用 GlobalScope.launch 做网络请求。结果用户退出页面了,请求还在跑,甚至回调里还在更新已经销毁的 UI——直接崩了。

注意:GlobalScope 不适合在 Android 的 Activity、Fragment 或 ViewModel 中使用。它的生命周期不受组件控制,容易造成内存泄漏。

那什么时候用?比如你需要在应用启动时做一些全局初始化,或者后台日志上传,这些跟 UI 无关的任务,可以考虑用 GlobalScope。

launch:点火发射,不关心结果

launch 是最常用的协程构建器。它启动一个协程,不返回任何结果。适合「只管做,不管返回值」的场景。

GlobalScope.launch {
    // 模拟耗时操作
    delay(1000)
    Log.d("TAG", "任务完成")
}

注意,delay() 是协程里的「睡眠」,它不会阻塞线程。这一点跟 Thread.sleep() 有本质区别。你想想看,如果阻塞了线程,那这个线程上的其他协程也得等着,多浪费。

实际项目中,我一般不会直接用 GlobalScope.launch,而是用 viewModelScope.launchlifecycleScope.launch。它们能自动跟随组件的生命周期取消,省心很多。

async:要结果?用这个

如果你需要协程执行完返回一个结果,那就用 async。它返回一个 Deferred 对象,你可以通过 await() 拿到结果。

val deferred = GlobalScope.async {
    delay(1000)
    "Hello from async"
}

// 在另一个协程中获取结果
GlobalScope.launch {
    val result = deferred.await()
    Log.d("TAG", result)
}

这里有个坑:await() 是挂起函数,必须在协程里调用。你不能在普通函数里直接 deferred.await(),会报错。

我曾经在项目里用 async 同时发起两个网络请求,然后等两个结果都回来再合并展示。代码写起来特别清爽,比回调嵌套舒服太多了。

小技巧:如果你需要并发执行多个任务,可以用 async 启动多个协程,然后统一 await。这样总耗时取决于最慢的那个任务,而不是串行累加。

withContext:切换线程的利器

withContext 的作用是:在指定调度器上执行代码块,并返回结果。它不会创建新的协程,而是挂起当前协程,切换到指定线程执行,执行完再切回来。

GlobalScope.launch(Dispatchers.Main) {
    // 主线程:更新 UI
    val result = withContext(Dispatchers.IO) {
        // IO 线程:做网络请求或数据库操作
        fetchDataFromNetwork()
    }
    // 回到主线程:更新 UI
    textView.text = result
}

你看,代码从上往下读,逻辑是线性的。但实际上,withContext 内部切换到了 IO 线程,执行完又切回主线程。这种写法比回调或 RxJava 的链式调用直观得多。

我个人习惯把 withContext 封装在 Repository 层里。这样 ViewModel 调用时,不需要关心线程切换,直接拿到结果就行。

Dispatchers:调度器,决定协程跑在哪个线程

调度器就是协程的「交通指挥员」。它告诉协程:你该在哪个线程池里跑。

调度器 用途 示例场景
Dispatchers.Main 主线程,用于 UI 操作 更新 TextView、显示 Dialog
Dispatchers.IO IO 密集型任务 网络请求、文件读写、数据库操作
Dispatchers.Default CPU 密集型任务 排序、解析 JSON、图像处理
Dispatchers.Unconfined 不指定线程,随当前线程执行 极少使用,一般用于测试

嗯,这里要注意:Dispatchers.Main 在 Android 上就是主线程。如果你在后台线程里更新 UI,会直接崩溃。所以一定要确保 UI 操作在 Main 调度器上执行。

我见过一个新手同事,在 Dispatchers.IO 里直接调了 textView.text = ...,结果 app 闪退。排查了半天才发现是线程问题。

知识体系总览

下面这张图帮你理清协程入门阶段的核心概念和关系:

Kotlin 协程入门核心概念 协程 Coroutine 协程构建器 launch 不返回结果 async 返回 Deferred 调度器 Dispatchers Main 主线程/UI IO 网络/文件 Default CPU 密集 作用域与工具 GlobalScope 全局生命周期 withContext 切换线程并返回结果 viewModelScope 跟随 ViewModel 生命周期 协程 = 构建器 + 调度器 + 作用域,三者配合实现优雅的异步编程

避坑指南:我曾经踩过的坑

最后分享几个我实际项目中遇到的坑,你遇到了能少走弯路:

  • GlobalScope 滥用:我曾经在一个 Fragment 里用 GlobalScope 启动协程做网络请求,结果用户快速返回再进入,协程还在跑,回调里更新了旧的 UI 引用——直接空指针。后来全部换成 lifecycleScope,问题解决。
  • async 忘记 await:用 async 启动协程后,如果不调用 await(),异常会被静默吞掉。你根本不知道任务失败了。所以记得要么 await,要么用 start 明确表示不关心结果。
  • withContext 嵌套过多:有人喜欢在 withContext 里再套 withContext,其实没必要。一个 withContext 就能切换到指定线程,执行完自动回来。嵌套只会让代码变乱。
  • Dispatchers.Main 做耗时操作:这个不用多说,主线程做耗时操作会卡 UI,甚至 ANR。记住:网络请求、数据库读写、文件操作,统统丢给 IO 调度器。

协程入门其实不难,关键是理解这几个核心概念怎么配合。你只要记住:launch 点火,async 要结果,withContext 切线程,Dispatchers 定位置。剩下的就是多写多练了。


公众号:蓝海资料掘金营,微信deep3321