美文网首页
Suspend函数与回调的互相转换

Suspend函数与回调的互相转换

作者: 代码我写的怎么 | 来源:发表于2022-12-12 14:59 被阅读0次

    前言

    我们再来一期关于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函数有哪几种情况

    这里的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()){
        情况1,2
        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背后的逻辑与挂起的细节,希望能帮到你!最后本篇还留下了一个小谜题,可以发挥你的理解在评论区说出你的想法!笔者之后会在评论区解答!

    作者:Pika
    链接:https://juejin.cn/post/7175752374458253373

    相关文章

      网友评论

          本文标题:Suspend函数与回调的互相转换

          本文链接:https://www.haomeiwen.com/subject/cmfcqdtx.html