前语

咱们再来一期关于kotlin协程的故事,咱们都知道在Coroutine没有出来之前,咱们关于异步成果的处理都是采用回调的方式进行,一方面回调层次过多的话,简单导致“回调阴间”,另一方法也比较难以保护。当然,咱们并不是否定了回调自身,回调自身一起也是具有很多优点的,比方契合代码阅览逻辑,一起回调自身也是比较可控的。这一期呢,咱们便是来聊一下,怎么把回调的写法变成suspend函数,一起怎么把suspend函数变成回调,然后让咱们愈加了解kotlin协程背面的故事

回调变成suspend函数

来一个回调

咱们以一个回调函数作为比方,当咱们normalCallBack在一个子线程中做一些处理,比方耗时函数,做完就会通过MyCallBack回调onCallBack,这里回来了一个Int类型,如下:

var myCallBack:MyCallBack?= null
interface MyCallBack{
    fun onCallBack(result: Int)
}
fun normalCallBack(){
    thread { 
        // 比方做一些工作
        myCallBack?.onCallBack(1)
    }
}

转化为suspend函数

此刻咱们可以通过suspendCoroutine函数,内部其实是通过创建了一个SafeContinuation并放到了咱们suspend函数自身(block自身)发动了一个协程,咱们之前在聊一聊Kotlin协程”低级”api 这篇文章介绍过

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }
}

这时候咱们就可以直接写为,然后将回调消除,变成了一个suspend函数。

suspend fun mySuspend() = suspendCoroutine<Int> {
    thread {
        // 比方做一些工作
        it.resume(1)
    }
}

当然,如果咱们想要支撑一下外部撤销,比方其时页面毁掉时,建议的网络请求自然也就不需求再请求了,就可以通过suspendCancellableCoroutine创建,里面的Continuation对象就从SafeContinuation(见上文)变成了CancellableContinuation,变成了CancellableContinuation有一个invokeOnCancellation便利,支撑在协程体被毁掉时的逻辑。

public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
        /*
         * For non-atomic cancellation we setup parent-child relationship immediately
         * in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
         * properly supports cancellation.
         */
        cancellable.initCancellability()
        block(cancellable)
        cancellable.getResult()
    }

此刻咱们就可以写出以下代码

suspend fun mySuspend2() = suspendCancellableCoroutine<Int> {
    thread {
        // 比方做一些工作
        it.resume(1)
    }
    it.invokeOnCancellation { 
        // 撤销逻辑
    }
}

suspend函数变成回调

见到了回调怎么变成suspend函数,那么咱们反过来呢?有没有方法?当然有啦!其时suspend函数中有很多种区别,咱们逐个区别一下

直接回来的suspend函数
suspend fun myNoSuspendFunc():Int{
    return 1
}
调用suspendCoroutine后直接resume的suspend函数
suspend fun myNoSuspendFunc() = suspendCoroutine<Int> {
        continuation ->
    continuation.resume(1)
}
调用suspendCoroutine后异步履行的suspend函数(这里异步可以是单线程也可以是多线程,跟线程自身无关,只要是异步就会触发挂起)
suspend fun myRealSuspendFunc() = suspendCoroutine<Int> {
    thread {
        Thread.sleep(300)
        it.resume(2)
    }

那么咱们来想一下,这里真实建议挂起的函数是哪个?通过代码其实咱们可以猜到,真实挂起的函数只要最终一个myRealSuspendFunc,其他都不是真实的挂起,这里的挂起是什么意思呢?咱们从协程的状况就可以知道,其时处于CoroutineSingletons.COROUTINE_SUSPENDED时,便是挂起状况。咱们回归一下,一个suspend函数有哪几种状况

Suspend函数与回调的互相转换

这里的1,2,3就分别对应着上文demo中的比方

  1. 直接回来成果,不需求进入状况机判别,由于自身就没有发动协程
  2. 进入了协程,但是不需求进行SUSPEND状况就已经有了成果,所以直接回来了成果
  3. 进入了SUSPEND状况,之后才能获取成果

这里咱们就不贴出来源码了,感兴趣可自己看Coroutine的实现,这里咱们要明确一个概念,一个Suspend函数的运行机制,其实并不依靠了协程自身。

对应代码体现便是,这个函数的回来成果可能便是直接回来成果自身,另一种便是通过回调自身通知外部(这里咱们还会以比方阐明)

suspend函数转换为回调

这里有两种状况,咱们分别以kotlin代码跟java代码表明:

kotlin代码

由于kotlin可以直接通过suspend的扩展函数startCoroutine发动一个协程,

fun myRealSuspendCallBack(){
    ::myRealSuspendFunc.startCoroutine(object :Continuation<Int>{
       其时环境
        override val context: CoroutineContext
            get() = Dispatchers.IO
        成果
        override fun resumeWith(result: Result<Int>) {
            if(result.isSuccess){
                myCallBack?.onCallBack(result.getOrDefault(0))
            }
        }
    })
}

其间Result便是一个内联类,归于kotlin编译器添加的装修类,在这里咱们无论是1,2,3的状况,都可以在resumeWith 中获取到成果,在这里通过callback回调即可

@JvmInline
public value class Result<out T> @PublishedApi internal constructor(
    @PublishedApi
    internal val value: Any?
) : Serializable {

Java代码

这里咱们更正一个误区,便是suspend函数只能在kotlin中运用/Coroutine协程只能在kotlin中运用,这个其实是错误的,java代码也可以调起协程,只不过麻烦了一点,至少官方是没有禁止的。 比方咱们需求调用startCoroutine,可直接调用

ContinuationKt.startCoroutine();

当然,咱们也可以直接调用suspend函数

Object result = CallBack.INSTANCE.myRealSuspendFunc(new Continuation<Integer>() {
    @NonNull
    @Override
    public CoroutineContext getContext() {
        这里发动的环境其实协程没有用到,读者们可以思考一下为什么!这里就当一个谜题啦!可以在谈论区说出你的主意(我会在谈论区解答)
        return (CoroutineContext) Dispatchers.getIO();
        //return EmptyCoroutineContext.INSTANCE;
    }
    @Override
    public void resumeWith(@NonNull Object o) {
        状况3
        Log.e("hello","resumeWith result is "+ o +" is main "+ (Looper.myLooper() == Looper.getMainLooper()));
          // 回调处理即可
          myCallBack?.onCallBack(result)
    }
});
if(result != IntrinsicsKt.getCOROUTINE_SUSPENDED()){
    状况12
    Log.e("hello","func result is "+ result);
      // 回调处理即可
      myCallBack?.onCallBack(result)
}

这里咱们需求留意的是,这里java代码比kotlin多了一个判别,一起resumeWith的参数不再是Result,而是一个Object

if(result != IntrinsicsKt.getCOROUTINE_SUSPENDED()){
}

这里脱去了kotlin给咱们添加的各种外壳,其实这便是真实的关于suspend成果的处理(只不过kotlin帮咱们包了一层)

咱们上文说过,suspend函数对应的三种状况,这里的1,2都是直接回来成果的,由于没有走到SUSPEND状况(IntrinsicsKt.getCOROUTINE_SUSPENDED())这里需求读者好好阅览上文,因此 result != IntrinsicsKt.getCOROUTINE_SUSPENDED(),就会直接走到这里,咱们就直接拿到了成果

if(result != IntrinsicsKt.getCOROUTINE_SUSPENDED()){
}

如果归于状况3,那么这里的result就不再是一个成果,而是其时协程的状况符号罢了,此刻当协程完成履行的时候(调用resume的时候),就会回调到resumeWith,这里的Object类型o才是通过SUSPEND状况的成果!

总结

通过咱们suspend跟回调的互相状况,可以明白了suspend背面的逻辑与挂起的细节,希望能帮到你!最终本篇还留下了一个小谜题,可以发挥你的了解在谈论区说出你的主意!笔者之后会在谈论区解答!

本文正在参与「金石方案 . 瓜分6万现金大奖」