为什么需要结构化
想象一个场景:应用发起了一个网络请求来获取数据,并将响应数据更新到 UI 上。如果用户在网络请求结束前关闭了页面,此时后台线程还活跃着,依旧在等待响应。并在得到响应后,尝试更新一个已经不存在的 UI 控件。
可以看到,这不仅会浪费资源,还可能会导致程序崩溃。
而协程通过结构化并发,能够将并发任务的管理变得容易。
CoroutineScope 与 Job:控制协程的生命周期
结构化并发的基础在于 CoroutineScope
和 Job
。
通过 launch
构建器启动协程后,会返回一个 Job
对象。我们可以把这个 Job
对象看作是一个协程,因为我们可以通过它来控制协程任务,比如取消协程(Job.cancel()
)、启动创建好的协程(Job.start()
)、查看协程的元数据。
只要在启动协程时,添加参数
start = CoroutineStart.LAZY
,协程就不会自动启动。
如果我们需要在用户关闭界面时取消协程任务,只需这样:
kotlin
private var job: Job? = null
// 启动协程
job = lifecycleScope.launch {
// ...耗时操作...
}
// 在Activity或Fragment中
override fun onDestroy() {
super.onDestroy()
job?.cancel()
}
当然,对于 lifecycleScope
来说,没必要手动去取消,因为它会在 onDestroy
中自动取消其所有子协程。如果我们想要自由控制某个协程的生命周期,就需要持有协程的 Job
对象来手动取消。
我们也可以通过 CoroutineScope
协程作用域的 cancel
函数来取消协程,区别在于 CoroutineScope.cancel()
会取消由它启动的所有协程。
一般没必要手动创建
CoroutineScope
,Android 已经提供好了lifecycleScope
和viewModelScope
这种与组件生命周期绑定的CoroutineScope
,当 Activity 或 Fragment 销毁时,会自动调用cancel()
。
同时,我们也可以把 launch
或 async
代码块内部的 CoroutineScope
接收者(this
)看作是协程对象。它是协程顶级的管理器,我们可以通过它来获取协程的所有功能和属性。
比如,我们可以在 launch
代码块中获取当前协程的协程调度器和 Job
对象。
kotlin
lifecycleScope.launch {
// 获取当前协程的 Job 对象
val job = this.coroutineContext[Job]
// 获取当前协程的调度器对象
val dispatcher = this.coroutineContext[CoroutineDispatcher]
}
可以看出,CoroutineScope
和 Job
并非并列关系,而是从属关系。CoroutineScope
包含了所有的功能,而 Job 只负责一部分。
结构化取消
CoroutineScope
与 Job
结合,带来了级联取消(Cascading Cancellation) 机制。
在一个协程中启动的协程,会自动变为该协程的子协程,这得益于父协程 block
块的隐式接收者 CoroutineScope
。我们在父协程内部调用的 launch
,实际上调用的是 this.launch
。
这个 CoroutineScope
(this
) 对象,是父协程内部专属的对象,而非启动父协程的 CoroutineScope
对象。这个对象在启动子协程时,携带了父协程的 Job
实例以及协程的上下文信息,所以能够形成父子层级关系。
kotlin
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
// 这个StandaloneCoroutine类型的coroutine对象,同时实现了Job和CoroutineScope接口
// 是方法返回的 Job 对象,也是代码块的接收者(this)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
所以,下面的代码会自动建立父子关系。
kotlin
lifecycleScope.launch { // 父协程
launch { // 子协程 1
delay(1000)
Log.d("Coroutine", "子协程 1 完成")
}
launch { // 子协程 2
delay(2000)
Log.d("Coroutine", "子协程 2 完成")
}
}
只要有一个 CoroutineScope
被取消,或是父 Job
被取消,那么取消的指令也会向下传给其内部启动的所有子协程、孙子协程...
但如果协程内部启动协程时,使用的 CoroutineScope
并不是当前协程的 this
,就构不成父子关系。例如下面的代码中,outerJob
并非 innerJob
的父协程,innerJob
、outerJob
是兄弟关系,它们的父协程是 CoroutineScope
内部创建的 Job
实例。
kotlin
val coroutineScope = CoroutineScope(Dispatchers.IO)
val outerJob = coroutineScope.launch {
val innerJob = coroutineScope.launch {
// ...
}
}
结构化完成
一个父协程会等待其所有子协程完成后,自己才会进入完成状态。
kotlin
lifecycleScope.launch {
val parentJob = launch { // 父协程
launch { // 子协程
delay(2000)
Log.d("Coroutine", "子协程完成")
}
Log.d("Coroutine", "父协程代码执行完毕")
}
parentJob.join() // 等待父协程及其所有子协程完成
Log.d("Coroutine", "所有工作都已结束")
}
所以我们在执行复杂的任务时,只需对父 Job
调用 join
,等到它返回时,就表示其内部所有的子任务都完成了。
并行任务的交互:async 和 await
如果我们要从并行的任务中获取结果,该怎么办?
比如同时请求用户信息和用户的好友列表,并将两者的结果合并显示到界面中。
这时,我们可以使用协程提供的构建器 CoroutineScope.async
,它和 launch
类似,只不过它会返回一个 Deferred<T>
对象(意为"延迟的值"),我们可以通过这个对象获取任务的结果,具体是调用它的 await()
挂起函数。
kotlin
lifecycleScope.launch {
val userDeferred = async {
delay(1000) // 模拟网络请求
"userInfo"
}
val friendsDeferred = async {
delay(1500) // 模拟网络请求
"Friends"
}
// ...两个任务并行执行...
// 挂起当前协程等待结果
val user = userDeferred.await()
val friends = friendsDeferred.await()
val result = "user: $user, friends: $friends"
println(result)
}
上述代码中 async
函数的调用都在父协程 launch
中,所以这两个任务都为父协程的子协程。只要父协程被取消,这两个并行的任务也会一起被取消。
如果某个任务的执行,需要等待另一个任务执行完毕,你也可以使用 Job.join()
。
kotlin
lifecycleScope.launch {
val initJob = launch {
Log.d("Coroutine", "初始化任务开始...")
delay(500) // 模拟初始化耗时
Log.d("Coroutine", "初始化任务完成")
}
launch {
// ...执行其他不依赖初始化的操作...
delay(200)
// 等待初始化完成
initJob.join()
// ...执行依赖初始化的操作...
Log.d("Coroutine", "开始执行依赖初始化的工作")
}
}
当然,通过 launch
启动的协程是获取不到结果的。
你也许会想到线程中的
join
,不过线程的结构化管理比较复杂,并不常用。