我们先来看一段代码,就拿最常见的网络请求为例子:
fun uploadFile(···) {
···
viewModelScope.launch {
//标记1
try {
val bean = uploadFileApi.uploadFile(···)
//标记2
···
} catch (e: Exception) {
···
}
}
}
首先问大家一个问题:标记1和标记2分别执行在哪个线程里面?
我们来查看viewModelScope源码:
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
注意Dispatchers.Main,可见通过viewModelScope启动的协程是运行在Android主线程上的。
所以标记1和标记2都是运行在主线程里面的。
我们再来看uploadFile这个函数:
suspend fun uploadFile(···):返回值
我们使用了Retrofit请求,这又是一个suspend函数。那么第二个问题来了:suspend有什么用?
suspend能够触发挂起吗?
private suspend fun test(){
print("宇哥好帅")
}
然后把这段代码放到标记1处:
fun uploadFile(···) {
···
viewModelScope.launch {
test()
try {
val bean = uploadFileApi.uploadFile(···)
//标记2
···
} catch (e: Exception) {
···
}
}
}
看截图:
你会发现这个suspend编译器提示我们是多余的,其实suspend函数本身是不会起到任何挂起的作用的,如果想要suspend函数起作用,那么一定是suspend函数内部调用了协程自带的挂起函数,比如说withContext等。suspend本身仅仅起到提示的作用,提示方法要进行耗时或IO操作。
再问一个问题:协程挂起的本质是什么?
很多博客文章说的云里雾里,其实就是三个字:切线程!
public final fun uploadFile(···): kotlin.Unit { /* compiled code */ }
我们去build里面看,uploadFile代码会变成这样,在方法内部,retrofit帮我们完成了切线程的操作。
再回到上面的test方法,我们改造一下:
这个时候编译器就不会提示suspend多余啦!
我们继续深入:为什么一个挂起函数只能在协程里面或是另一个挂起函数里面声明?
挂起是为了什么?挂起无非就是切了一个线程去执行一些任务,但是挂起后是会恢复的。而与挂起对应的恢复是在协程里面实现的。所以挂起函数只能声明在协程里,或是另一个挂起函数里,向上追溯的话,最终还是在协程里。
好多文章再说,协程比线程更高效?
答案是并不会!就比如说网络请求,协程只是切一个线程去完成网络请求,网络请求主要是IO操作,就是会有一个线程去进行IO操作,完成后再切回主线程。只是对主线程不阻塞,并不是对IO线程不阻塞。
理解了这些,我们就来一起看看协程的核心原理吧。
要讲挂起恢复的原理,就不得不先说一下CPS(Continuation-Passing-Style, 续体传递风格)。
我们编译后的挂起函数都会多一个参数:
continuation: Continuation<T>
假设有这样一段代码:
val a = a()
val y = A(a) // 挂起点 #1
b()
val z = B(a, y) // 挂起点 #2
c(z)
A方法和B方法为挂起方法。编译后字节码通过工具查看:
// 状态机当前状态
int label = 0
// 协程的局部变量
A a = null
Y y = null
void resumeWith(Object result) {
if (label == 0) goto L0
if (label == 1) goto L1
if (label == 2) goto L2
else throw IllegalStateException()
L0:
// 这次调用,result 应该为空
a = a()
label = 1
result = A(a)
if (result == COROUTINE_SUSPENDED) return // 如果挂起了执行则返回
L1:
y = (Y) result
b()
label = 2
result = B(a, y)
if (result == COROUTINE_SUSPENDED) return // 如果挂起了执行则返回
L2:
Z z = (Z) result
c(z)
label = -1 // 没有其他步骤了
return
}
注意:COROUTINE_SUSPEND表示协程被挂起,挂起函数执行结束后,状态会通过CAS更改为RESUMED。每次挂起函数执行结束后都会调用Continuation的resumeWith方法。