协程环境搭建:从依赖引入到 Scope 创建
做协程开发,第一步不是写代码,是把架子搭好。我见过不少同学在项目里直接 copy 网上代码,结果编译报错半天找不到原因——说白了,就是依赖没配对。
这一章,我们先把基础环境铺平。我会带你从头创建一个 Kotlin 项目,然后引入协程核心库,最后创建一个 CoroutineScope。相信我,这一步如果偷懒,后面排查启动时序问题时会多花十倍的时间。
2.1 新建 Kotlin 项目并添加协程依赖
我用的是 IntelliJ IDEA,创建一个普通的 Kotlin/JVM 项目就行。如果你是用 Android Studio,操作也差不多。这里我假设你已经有了一个项目,我们直接看 build.gradle.kts 里的依赖配置。
核心依赖:org.jetbrains.kotlinx:kotlinx-coroutines-core
打开你的 build.gradle.kts(或 build.gradle),在 dependencies 块里加入以下几行:
dependencies {
// Kotlin 标准库(通常已默认)
implementation("org.jetbrains.kotlin:kotlin-stdlib")
// 协程核心库
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
// 如果你用 Android,还需要添加这个(后面章节会细讲)
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
为什么只加 core 就够?因为协程的核心 API——比如 launch、async、CoroutineScope——都在这个包里。Android 版多了一些调度器和生命周期绑定,我们现在先不管。
我个人习惯在项目里统一管理协程版本号,比如在 gradle.properties 里定义:kotlinCoroutinesVersion=1.8.1,然后在 dependencies 里引用 ${kotlinCoroutinesVersion}。这样升级版本时改一个地方就行。
添加完后,记得点一下「Sync Now」或者执行 ./gradlew build。等一下……嗯,我第一次做这一步时卡在了版本不兼容上。后来发现 Kotlin 1.9.x 对应协程 1.7.x 以上基本都兼容,但如果你的 Kotlin 版本特别老,建议先升级 Kotlin。
2.2 理解 CoroutineScope:它到底是个什么东西?
你可以把 CoroutineScope 想象成一个「协程容器」。每个协程都必须生活在某个 scope 里。scope 负责追踪所有它启动的协程,当 scope 被取消时,内部所有协程都会被自动取消。
来看一个简单例子:
// 创建一个空的协程作用域
val myScope = CoroutineScope(Dispatchers.Default)
这里 Dispatchers.Default 是调度器,告诉协程跑在哪个线程池。后面我们专门讲调度器,现在你只要记住:scope 创建时必须指定一个调度器(或使用 SupervisorJob() 组合)。
为什么不是直接用 GlobalScope?我曾经在早期项目里图省事,直接用 GlobalScope.launch,结果 activity 销毁了协程还在跑,内存泄漏得一塌糊涂。所以 根本不建议在业务代码里使用 GlobalScope,它没有生命周期管理,说白了就是个全局静态 scope,你几乎用不到它。
| Scope 类型 | 生命周期 | 使用场景 |
|---|---|---|
GlobalScope | 应用进程级别 | 极少使用,仅限临时工具类或测试 |
MainScope | 绑定到 UI 组件(如 Activity) | Android 的 UI 层(现在更推荐 lifecycleScope) |
自定义 CoroutineScope | 由开发者手动控制 | 大部分业务场景,灵活可控 |
千万不要轻易用 GlobalScope。我见过有人把它当默认选项,结果协程失控。记住:scope 的生命周期应当和它的使用者一致。
2.3 创建自定义 CoroutineScope 的最佳实践
我通常会在业务类里直接声明一个 scope 属性,并在不需要时主动取消它。比如:
class MyRepository {
// 使用 SupervisorJob + Dispatchers.IO 组合
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun loadData() {
scope.launch {
// 在这里进行 IO 操作
delay(1000)
println("数据加载完成")
}
}
// 在 Repository 不再使用时调用
fun destroy() {
scope.cancel()
}
}
注意我用了 SupervisorJob() 而不是 Job()。区别在于:如果 launch 里某个子协程抛异常,SupervisorJob 不会把这个异常向上传播导致整个 scope 取消。这在多个独立子任务并行时非常有用。你想想看,如果下载图片的子协程因为网络问题抛了个异常,结果把整个 Repository 里的数据加载协程都取消了——多冤啊。
2.4 时序问题从 scope 创建就开始了
为什么环境搭建跟启动时序有关?因为 scope 创建的时机和生命周期直接影响协程何时启动、何时取消。举个例子:
class MyViewModel {
private var scope: CoroutineScope? = null
fun onStart() {
// 延迟创建:假设 onStart 在某个回调里被触发
scope = CoroutineScope(Dispatchers.Main)
scope?.launch {
// 如果在这里访问了尚未初始化的数据,就会出现时序问题
}
}
fun onStop() {
scope?.cancel()
scope = null
}
}
你看,如果 onStart 和 onStop 的调用顺序出现了乱序,或者 scope 在创建后还没来得及启动协程就被取消了,那就会导致协程根本没执行。我们在后续章节里会反复遇到这类问题,而根源往往就是 scope 的创建和取消没有对齐生命周期。
核心原则:scope 的创建应该尽可能早(比如在对象构造时),取消应该尽可能晚(不使用时立即取消)。
2.5 知识结构梳理:从依赖到作用域
下面这张图总结了我们这一章的脉络。你把它理解成一个「环境搭建三步走」的流程:先配依赖,再建 scope,最后用 scope 启动协程。
这张图你可以截图存着,后面遇到协程启动时序问题时,先回来看看 scope 是在哪个阶段创建的——很多时候问题就出在阶段 2 和阶段 3 之间的衔接上。
2.6 本章小结
我们做了三件事:
- 添加了
kotlinx-coroutines-core依赖 - 理解了
CoroutineScope的本质及其生命周期管理 - 创建了自定义 scope,并掌握了创建时机对时序的影响
这些就是协程大楼的地基。下一块砖,我们会开始讨论协程启动的具体时机——比如 launch 一调用就立即执行吗?如果不是,那延迟在哪里?
不过别急,在动手写第一个协程之前,请确保你环境里的依赖版本是匹配的。我偶尔会看到有人把 kotlinx-coroutines-core 写成了 kotlinx-coroutine-core(少了一个 s),编译时对着红字发半天呆……嗯,别问我怎么知道的。