Kotlin协程详解——拦截器ContinuationInterceptor

 ContinuationInterceptor

它的作用是 [在代码往下执行之前先拦截住,做点别的工作,再继续执行]

看到Interceptor相信第一印象应该就是拦截器,例如在Okhttp中被广泛应用。自然在协程中ContinuationInterceptor的作用也是用来做拦截协程的。

下面来看下它的实现。

public interface ContinuationInterceptor : CoroutineContext.Element {
    /**
     * The key that defines *the* context interceptor.
     */
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>

    /**
此函数返回一个包装了原始[continuation](续体)的新续体,从而拦截所有的恢复操作。
当需要时,协程框架会调用此函数,并且对于原始[continuation]的每个实例,
生成的续体会被内部缓存。

如果该函数不需要拦截特定的[continuation],它可以直接返回原始的[continuation]。
当原始[continuation]完成时,如果它之前被拦截过(即interceptContinuation之前
返回了一个不同的续体实例),协程框架会调用[releaseInterceptedContinuation]函数,
并将生成的续体作为参数传递。
     */
    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>

    ...
}

只给出了关键部分,ContinuationInterceptor继承于CoroutineContext.Element,所以它也是CoroutineContext,同时提供了interceptContinuation方法,先记住这个方法后续会用到。

分析了CoroutineContext的内部结构,当时提到了它的plus方法,就是下面这段代码

public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
        context.fold(this) { acc, element ->
            val removed = acc.minusKey(element.key)
            if (removed === EmptyCoroutineContext) element else {
                // make sure interceptor is always last in the context (and thus is fast to get when present)
                val interceptor = removed[ContinuationInterceptor]
                if (interceptor == null) CombinedContext(removed, element) else {
                    val left = removed.minusKey(ContinuationInterceptor)
                    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                        CombinedContext(CombinedContext(left, element), interceptor)
                }
            }
        }

在这里第一次看到了ContinuationInterceptor的身影,当时核心是为了分析CoroutineContext,所以只是提了plus方法每次都会将ContinuationInterceptor添加到拼接链的尾部。

不知道有没有老铁想过这个问题,为什么要每次新加入一个CoroutineContext都要调整ContinuationInterceptor的位置,并将它添加到尾部?

这里其实涉及到两点。

  • 其中一点是由于CombinedContext的结构决定的。它有两个元素分别是leftelement。而left类似于前驱节点,它是一个前驱集合,而element只是一个纯碎的CoroutineContext,而它的get方法每次都是从element开始进行查找对应KeyCoroutineContext对象;没有匹配到才会去left集合中进行递归查找。

所以为了加快查找ContinuationInterceptor类型的实例,才将它加入到拼接链的尾部,对应的就是element

  • 另一个原因是ContinuationInterceptor使用的很频繁,因为每次创建协程都会去尝试查找当前协程的CoroutineContext中是否存在ContinuationInterceptor。例如我们通过launch来看协程的启动。
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

如果你使用launch的默认参数,那么此时的Coroutine就是StandaloneCoroutine,然后调用start方法启动协程。

public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
    initParentJob()
    start(block, receiver, this)
}

start中进入了CoroutineStart,对应的就是下面这段代码

public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>) =
    when (this) {
        CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion)
        CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion)
        CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
        CoroutineStart.LAZY -> Unit // will start lazily
    }

因为我们使用的是默认参数,所以这里对应的就是CoroutineStart.DEFAULT,最终来到block.startCoroutineCancellable

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
    }

在这里我们终于看到了intercepted

首先通过createCoroutineUnintercepted来创建一个协程(内部具体如何创建的这篇文章先不说,后续文章会单独分析),然后再调用了intercepted方法进行拦截操作,最后再resumeCancellable,这个方法最终调用的就是ContinuationresumeWith方法,即启动协程。

所以每次启动协程都会自动回调一次resumeWith方法。

今天的主题是ContinuationInterceptor所以我们直接看intercepted

public expect fun <T> Continuation<T>.intercepted(): Continuation<T>

发现它是一个expect方法,它会根据不同平台实现不同的逻辑。因为我们是Android所以直接看Android上的actual的实现

public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    (this as? ContinuationImpl)?.intercepted() ?: this

最终来到ContinuationImplintercepted方法

public fun intercepted(): Continuation<Any?> =
    intercepted
        ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
            .also { intercepted = it }

在这里看到了熟悉的context,获取到ContinuationInterceptor实例,并且调用它的interceptContinuation方法返回一个处理过的Continuation

多次调用 intercepted,对应的 interceptContinuation只会调用一次。

所以ContinuationInterceptor的拦截是通过interceptContinuation方法进行的。既然已经明白了它的拦截方式,我们自己来手动写一个拦截器来验证一下。

val interceptor = object : ContinuationInterceptor {
 
    override val key: CoroutineContext.Key<*> = ContinuationInterceptor
 
    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        println("intercept todo something. change run to thread")
        return object : Continuation<T> by continuation {
            override fun resumeWith(result: Result<T>) {
                println("create new thread")
                thread {
                    continuation.resumeWith(result)
                }
            }
        }
    }

}
 
println(Thread.currentThread().name)
 
lifecycleScope.launch(interceptor) {
    println("launch start. current thread: ${Thread.currentThread().name}")
    
    withContext(Dispatchers.Main) {
        println("new continuation todo something in the main thread. current thread: ${Thread.currentThread().name}")
    }
    
    launch {
        println("new continuation todo something. current thread: ${Thread.currentThread().name}")
    }
    
    println("launch end. current thread: ${Thread.currentThread().name}")
}

这里简单实现了一个ContinuationInterceptor,如果拦截成功就会输出interceptContinuation中对应的语句。下面是程序运行后的输出日志。

main
// 第一次launch
intercept todo something. change run to thread
create new thread
launch start. current thread: Thread-2
new continuation todo something in the main thread. current thread: main
create new thread
// 第二次launch
intercept todo something. change run to thread
create new thread
launch end. current thread: Thread-7
new continuation todo something. current thread: Thread-8

分析一下上面的日志,首先程序运行在main线程,通过lifecycleScope.launch启动协程并将我们自定义的intercetpor加入到CoroutineContext中;然后在启动的过程中发现我们自定义的interceptor拦截成功了,同时将原本在main线程运行的程序切换到了新的thread线程。同时第二次launch的时候也拦截成功。

到这里就已经可以证明我们上面对ContinuationInterceptor理解是正确的,它可以在协程启动的时候进行拦截操作。

下面我们继续看日志,发现withContext并没有拦截成功,这是为什么呢?注意看Dispatchers.Main。这也是接下来需要分析的内容。

另外还有一点,如果细心的老铁就会发现,launch startlaunch end所处的线程不一样,这是因为在withContext结束之后,它内部还会进行一次线程恢复,将自身所处的main线程切换到之前的线程,但为什么又与之前launch start的线程不同呢?

大家不要忘了,协程每一个挂起后的恢复都是通过回调resumeWith进行的,然而外部launch协程我们进行了拦截,在它返回的ContinuationresumeWith回调中总是会创建新的thread。所以发生这种情况也就不奇怪了,这是我们拦截的效果。

整体再来看这个例子,它是不是像一个简易版的协程的线程切换呢。

推荐文章

https://zhuanlan.zhihu.com/p/301494587

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闲暇部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值