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的结构决定的。它有两个元素分别是left与element。而left类似于前驱节点,它是一个前驱集合,而element只是一个纯碎的CoroutineContext,而它的get方法每次都是从element开始进行查找对应Key的CoroutineContext对象;没有匹配到才会去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,这个方法最终调用的就是Continuation的resumeWith方法,即启动协程。
所以每次启动协程都会自动回调一次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
最终来到ContinuationImpl的intercepted方法
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 start与launch end所处的线程不一样,这是因为在withContext结束之后,它内部还会进行一次线程恢复,将自身所处的main线程切换到之前的线程,但为什么又与之前launch start的线程不同呢?
大家不要忘了,协程每一个挂起后的恢复都是通过回调resumeWith进行的,然而外部launch协程我们进行了拦截,在它返回的Continuation的resumeWith回调中总是会创建新的thread。所以发生这种情况也就不奇怪了,这是我们拦截的效果。
整体再来看这个例子,它是不是像一个简易版的协程的线程切换呢。
推荐文章
1271

被折叠的 条评论
为什么被折叠?



