什么是协程?
- 协程本质上可以认为是运行在线程上的代码块,是一套由kotlin官方提供的线程API(类似Java的Executor) 操作。
- 此处讨论的是跑在JVM上的协程。 JVM原本没有协程这个东西,所以其实协程并没有从底层创建出新的东西,它无非就是对线程的一个封装。
为什么用协程,协程有什么好处?
- 简单啊!!!
- 可以用同步的代码写出异步的操作, 或者说用看起来阻塞的方式写出不阻塞的代码
eg: 需要请求两个api, 然后拿着这两个api的值做一个整合,最后在页面显示出来
- 如果用回调的方式,写法如下
//回调方式
getCard(canId)
.enqueue(obj : Callback<Bitmap> {
...
override fun onResponse(call:Call<Bitmap>,
response: Resoinse<Bitmap>) {
val card = resonse.body()
getSofList(canId)
.enqueue(obj : Callback<Bitmap> {
...
override fun onResponse(call:Call<Bitmap>
response: Resoinse<Bitmap>) {
val sofList = response.body()
show(suspendMerge(cardDetail, sofList)) // 合并两个信息 & UI显示
})
}
})
👆这个代码就是用回调的方式实现的,它有什么问题呢?
两个网络请求原本是可以并行的网络请求,只是他们需要一起展示出来。如果用回调的方法,就会被做成一个串行的,然后网络等待多了一倍。
- 那我们再用Kotlin的协程实现一下呢?
//协程
suspend fun showCard(){
val cardDetail = getCard(canId) // 获取卡详情
val sofList = getSofList(canId) // 获取银行卡信息
val cardInfo = suspendMerge(cardDetail.await, sofList.await) // 合并两个信息
show(cardInfo) // UI显示
}
哇塞!是不是炒鸡简洁明了😄 对了,这就是为什么要用kotlin协程。这个里面干了啥我们不用管,反正此刻只需要知道,协程就让原本用callback写起来很复杂的东西变的整洁干净,清晰明了就好了。
挂起函数(suspend函数)
刚上面就出现了一个关键字 suspend, 那这个加了suspend这个词的函数又是啥呢? -> 挂起函数
那什么是挂起函数?
就是在普通的函数前面加一个关键字 suspend
一般函数写法:
fun getCard() {
......
}
挂起函数写法:
suspend fun getCard() {
......
}
suspend关键字作用:
提醒 这个函数的调用者: 我是一个耗时的函数, 请在协程中调用我。
挂起函数作用
说了半天,为什么要用挂起函数?
挂起的意思是:暂时从当前线程脱离,一会儿再切/恢复回来(resume)
当一个函数从当前线程A挂起后,就出现了两条线:
- A线程:该执行什么执行什么,比如刷新界面,或者没有其他事情可做就被回收再利用
- 挂起函数:切换到指定线程,并从被挂起的那行代码开始,继续向下执行代码,执行完以后,再切回到A线程
举个例子吧:
安卓开发中,主线程Main正在执行任务,执行到一个网络请求N(挂起函数)时,N脱离主线程,到指定的IO线程做网络请求。 然后线程Main将继续渲染界面(比如转圈圈loading),等N在IO线程执行结束后,切回Main线程,拿着刚请求完的值,继续做后序操作(比如loading结束,显示请求回来的数据结果)
挂起函数问题
- 挂起的对象是协程
- 挂起函数只能在另一个挂起函数或者协程中被调用:why?
因为,切走再恢复(resume)回来是协程的东西,所以只能在协程中调用
原理
好了,现在知道什么是suspend函数了,但是具体是实现或者它原理是什么样的呢?我们来慢慢捋一捋。
挂起函数就是在普通的函数前面加一个关键字suspend,然而Java 平台并没有 suspend 关键字,也没有 suspending 机制,那kotlin又是怎么实现了可以用看起来阻塞的方式实现不阻塞的代码的呢?
CPS 变换
Kotlin 编译器会对 suspending 方法做特殊处理,对代码进行转换,从而实现 suspending 机制。
那 Kotlin 编译器做了哪些处理?简单说,主要做了下面这三项处理:
处理一:增加 Continuation 类型入参,返回值变为 Any?
处理二:生成 Continuation 类型的匿名内部类
处理三:对 suspending 方法的调用变为 switch 形式的状态机
以上引用于作者: 编走编想
那我们来看看最根本的变化:
//挂起函数的函数签名
suspend fun <T> CompletableFuture<T>.await():
变化后
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
可以看到,编译器做了什么? 传入了一个Continuation并且返回类型变为了Any?
- 那
Continuation是什么?
一个接口:Kotlin 的接口可以既包含抽象方法的声明也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。 (Kotlin interface官方解释)
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
Any?是什么? 表示返回的类型可以是任意类型。
那这里就有一个问题,比如一个函数,原本返回的是String, 为什么这里变成Any了?? 因为编译以后,除了要返回原本要返回的值,还要返回一个标记——COROUTINE_SUSPENDED,所以找不到一个对应的类型来和返回结果匹配,因此变成类一个Any?类型。?表示可以为空。
讲原理以前我们先看看怎么写回调: 正常打印一个6
private fun printSix() {
println("6")
}
Kotlin中用回调实现:向函数中传入一个Printer,并且把具体的实现方式暴露给调用这个方法的人,
interface Printer{
fun print()
}
private fun printSix(printer: Printer) {
printer.print()
}
fun callPrintSix() {
printSix(object : Printer {
override fun print() {
//具体的实现暴露给调用者
println("6")
Log.i("tag", "6")
...
}
})
}
那么知道Kotlin怎么写回调以后,再回过头看看我们suspend函数的变化:
suspend fun getCard(cardNum : String) {
val card = api.getCard(cardNum)
}
经过编译器以后:
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
fun getCard(cardNum: String,continuation: Continuation) {
//new一个continuation
val newContinuation = object: Continuation({
override resumeWith(..) {
getCard(this) //将new出来的这个newContinuation传入getCard函数自己
//这里看出来了吗,其实就很像是一个递归
}
})
//状态机
switch (newContinuation.label) {
case 0:
newContinuation.cardNum = cardNum
newContinuation.label = 1
return api.getCard(cardNum, newContinuation)
case 2:
val card = newContinuation.result
return card
}
}
其实仔细读会发现其实kotlin协程就是通过回调+递归+状态机实现了我们最终的能够用简单且同步的方式写出异步的代码。
小结:每个 suspend 方法编译后会增加一个 Continuation 类型的参数。每个 suspend函数都有一个回调自己的 Continuation 实现类,并且这个类会被传递给这个 suspend函数所调用的其它 suspending 方法,这些子方法可以通过 Continuation 回调父方法用来恢复。
cps (continuation-passing style)
续体编程风格,其实CPS就是一种编程风格,也就是一种回调风格。
- 其中continuation是一个接口,里面包含了一个
resume函数,可以通过这个函数将线程恢复到初始线程。比如一开始在Main线程,切到了IO线程去做请求,最后在Main函数调用continuation.resume()那么就会自然切回到main函数,就因为是Main调用的。 - 根据上面的代码,我们会发现每次都会将父级的continuation传入子方法,依次这样一级一级传下去,如果return的结果没有之前说的那个标记
COROUTINE_SUSPENDED, 说明执行已经结束了,那么将调用最初一层层带下去的父continuation.resume()并返回结果。 (对应到状态机中就是没有label这个值,如果还有后续步骤,就会继续return包含label和result的值) 以下是网上找的图
总结
本质:
-
协程本质就是线程框架,是线程上的一个代码块。没有创造新的东西。
-
通过编译器实现CPS变化,从而让开发人员可以以很简化的,同步的方式写出异步的代码。
原理: -
每个SUSPEND函数编译后都会有一个Continuation 类型的入参,用于实现回调。返回值变为 Any? 类型,既可以表示真实的结果,也可表示 Coroutine 的执行状态。
-
编译器会为这个 suspend方法生产一个类型为 Continuation 的匿名内部类(扩展 CoroutineImpl),用于对这个 suspend 方法自身的回调,并可以在这个 suspend 方法执行完毕之后,回调这个 suspend 方法上一级的父方法。
-
最后,这个 suspend方法如果调用其它 suspend方法,会将这些调用转换为一个 switch 形式的状态机,每个case表示对一个 suspend 子方法的调用或最后的return。同时,生成的 Continuation 匿名内部类会保存下一步需要调用的 suspending 方法的 label 值,表示应该执行 switch 中的哪个 case,从而串联起整个调用过程。