接《Android开发者快速上手Kotlin(五) 之 协程语法初步》文章继续。
13 协程上下文和拦截器
我们在完成了上一篇文章的学习后,你是不是已经大概清楚协程是什么和怎样使用了。但是你可能还存在着疑惑,既然协程没有异常的能力,使用过程中还需要自己去创建线程,那为什么不直接使用线程直截了当,还要绕一个大圈写那么多语法来完成一个看似高端却没什么实际意义的玩意?其实大多数人在初学习协程时都会存在这样的疑问,但是我想跟你说,请语法先学习,凡事不着急,Demo的目的是为了学习基础语法,它离实际应用还有一段很长的距离,而且基本语法学习完后还有一些封装好的框架,到那时你一定会感叹协程是多么的方便和好用。
回到正题,本篇文章我们继续来进一步学习协程的上下文和拦截器,学习完本篇文章后,你便可以消除上面的疑惑,因为协程可以帮助了你在多线程开发中完美地切换线程,耗时操作直接交由子线程完成,难道这不是我们日常开发中最常使用的场景吗。
13.1 上下文(CoroutineContex)
我们在上一篇文章中的Demo中创建和启动协程调用了startCoroutine 函数时需要传入了一个Continuation,Continuation里有两个成员,其中有一个属性叫CoroutineContext,前面提到 CoroutineContext也是一个接口,表示运行上下文或者叫协程上下文,当时因为我们不对它作处理所以给它赋于EmptyCoroutineContext。那么Continuation到底是什么,叫上下文的是不是很高级的东西?其实CoroutineContext就是一个在执行过程中携带数据的载体对象,或者你可以用最简单的理解,它就是一个用Key作索引,Element作元素的集合而已,一般用于数据从协程外层传递协程内部。自定义CoroutineContext一般需要继承AbstractCoroutineContextElement。
13.1.1 上下文Demo1
假设现在我们需要在主线程中传递一个Boolean值以协程内部,然后在协程内部经过一系列的逻辑处理后返回相应的结果,那么我们上一篇文章的入门Demo可以这样改:
fun main() {
log("Main函数开始")
coroutineDo(ParameterContext(true)) { // 传入一个自定义的Context
val result = blockFun()
log("异步方法返回结果:${result}")
result
}
log("Main函数结束")
}
fun <T> coroutineDo(coroutineContext: CoroutineContext, block: suspend () -> T) {
block.startCoroutine(object : Continuation<T> {
override val context: CoroutineContext = EmptyCoroutineContext + coroutineContext // 如果你需要多个CoroutineContext还可以使用加号进行添加
override fun resumeWith(result: Result<T>) {
log("收到异步结果:${result}")
}
})
}
suspend fun blockFun() = suspendCoroutine<String> { continuation ->
Thread {
val isSuccess = continuation.context[ParameterContext]!!.isSuccess // 获取传入的context元素
log("异步开始")
Thread.sleep(2000)
if (isSuccess) {
continuation.resumeWith(Result.success("异步请求成功"))
} else {
continuation.resumeWith(Result.failure(Exception()))
}
}.start()
}
class ParameterContext(val isSuccess: Boolean) : AbstractCoroutineContextElement(Key) { // 创建一个自定义的Context
companion object Key : CoroutineContext.Key<ParameterContext>
}
fun log(msg: String) {
println("【${Thread.currentThread().name}】$msg")
}
上述Demo中,我们新建了一个ParameterContext,用于最外层在协程开始的时候传入,并附加一个isSuccess的值。Context经过startCoroutine里的Coroutine中可通过+来进行add,最后在suspend函数内部通过continuation.context[Key]?.Element的方式获取值。
13.2 拦截器(ContinuationInterceptor)
ContinuationInterceptor是一个接口,被称为协程控制拦截器,因为它可以对协程上下文所在的协程的Continuation进行拦截,所以它可以用来处理线程的切换。若要使用拦截器就需要在自定义CoroutineContext的基础上再进行继承ContinuationInterceptor接口并实现interceptContinuation函数。
13.2.1 拦截的时机
还记得startCoroutine的源码吗?它是接收一个Continuation,接着创建一个新的Continuation,然后再调用了一个intercepted函数。
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}
intercepted函数会走到ContinuationImpl# intercepted:
internal abstract class ContinuationImpl(completion: Continuation<Any?>?, private val _context: CoroutineContext?) : BaseContinuationImpl(completion) {
// ……
public fun intercepted(): Continuation<Any?> = intercepted?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this).also { intercepted = it }
// ……
}
我们知道,通过continuation.context[Key]?. Element便可以获到Context中Key对应的Element对象,上面源码中可见intercepted函数最后会调用到我们自定义的CoroutineContex里的interceptContinuation函数是。所以调用startCoroutine函数来启动协程,实际上就是启动了我们自定义CoroutineContex里拦截后的Continuation。
13.2.2 拦截器Demo2
我们一直在使用协程中,都是通过自己在协程内部去创建Thread进行实现异步逻辑,这样无异于自己切换线程。我们在开始介绍概念时就一直强调协程是没有异步能力,但是拥有切线程的能力,而这个切线程的能力就是通过拦截器来实现的。我们可以在开始协程后通过拦截原来主线程中的Continuation,然后返回一个新的Continuation,在新的Continuation里我们通过线程池来完成我们的异步逻辑。那么我们根据上一节的Demo1可以这样改:
fun main() {
log("Main函数开始")
coroutineDo(ParameterContext(true)) {
val result = blockFun()
log("异步方法返回结果:${result}")
result
}
log("Main函数结束")
}
fun <T> coroutineDo(coroutineContext: CoroutineContext, block: suspend () -> T) {
block.startCoroutine(object : Continuation<T> {
// override val context: CoroutineContext = EmptyCoroutineContext + coroutineContext
override val context: CoroutineContext = AsyncContext() + coroutineContext
override fun resumeWith(result: Result<T>) {
log("收到异步结果:${result}")
}
})
}
suspend fun blockFun() = suspendCoroutine<String> { continuation ->
// Thread {
val isSuccess = continuation.context[ParameterContext]!!.isSuccess
log("异步开始")
Thread.sleep(2000)
if (isSuccess) {
continuation.resumeWith(Result.success("异步请求成功"))
} else {
continuation.resumeWith(Result.failure(Exception()))
}
// }.start()
}
class ParameterContext(val isSuccess: Boolean) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<ParameterContext>
}
fun log(msg: String) {
println("【${Thread.currentThread().name}】$msg")
}
val singleThreadExecutor = ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue<Runnable>())
class AsyncContext() : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
return ThreadPoolContinuation(continuation)
}
}
class ThreadPoolContinuation<T>(private val continuation: Continuation<T>) : Continuation<T> {
override val context: CoroutineContext = continuation.context
override fun resumeWith(result: Result<T>) {
singleThreadExecutor.execute { continuation.resumeWith(result) }
}
}
13.2.3 运行结果
【main】Main函数开始
【main】Main函数结束
【pool-1-thread-1】异步开始
【pool-1-thread-1】异步方法返回结果:异步请求成功
【pool-1-thread-1】收到异步结果:Success(异步请求成功)
13.2.4 解说
上面Demo2中前面部分跟上一节的Demo1几乎是一样的,区别在于在startCoroutine传入的Continuation中的CoroutineContext由EmptyCoroutineContext换成了我们自定义的AsyncContext,以及blockFun函数中注释了Thread的使用。从运行结果可见,其效果还是跟前面Demo是一样的。请往下看新增的代码。
- 新增了一个线程池对象singleThreadExecutor,异步逻辑就是通过它来完成的;
- 新增了一个AsyncContext类,上面我们了解到,继承AbstractCoroutineContextElement是为了自定义Context,继承ContinuationInterceptor是为了对Continuation进行拦截,里面的interceptContinuation函数就是接收原来的Continuation,然后返回一个新的Continuation。
- 新增ThreadPoolContinuation类,它就是新返回的Continuation,它在resumeWith函数中使用了线程池来完成工作逻辑。
12.3 Demo3
我们来思考一下,是不是可以将上面新增的线程池、AsyncContext类和ThreadPoolContinuation类进行一下封装,然后在所有的异常逻辑中进行复用,那么往后我们要实现一些耗时操作时,只需要写出一个普通的函数blockFun就可以了?没错的,所以我们的代码又可以进一点进化成这样:
fun main() {
log("Main函数开始")
coroutineDo(ParameterContext(true)) {
try {
val result = blockDo {// coroutineContext: CoroutineContext -> // 如果不使用this,便需要携带参数coroutineContext
val isSuccess = this[ParameterContext]!!.isSuccess // 通过this获取外部传入的context元素
blockFun(isSuccess)
}
log("异步方法返回结果:${result}")
result
} catch (e: Exception) {
e.printStackTrace()
}
}
log("Main函数结束")
}
// 使用扩展函数的形式,使block带this对象,这里也可以在括号里加 coroutineContext: CoroutineContext 参数把Context带出去,如:block: (coroutineContext: CoroutineContext) -> T
suspend fun <T>blockDo(block: CoroutineContext.() -> T) = suspendCoroutine<T> { continuation ->
try {
continuation.resumeWith(Result.success(block(continuation.context)))
} catch (e: Exception) {
continuation.resumeWith(Result.failure(Exception()))
}
}
fun blockFun(isSuccess: Boolean): String {
log("异步开始")
Thread.sleep(2000)
if (isSuccess) {
return "异步请求成功"
} else {
throw Exception()
}
}
// 不变的函数省略……
我们将原来的blockFun函数进行拆分成两个函数,其中将suspend的责任交给了blockDo函数,它的参数并使用了扩展函数的形式使block带上this对象,这样在调用处其表达式内部更可以通过this来取其context的元素了。最后拆分后的blockFun就变成了一个普通的函数,所以在main调用处便多出了一层blockDo表达式的嵌套。
异常捕获
看到这你是不是又多出一个疑问,为什么在blockDo上层加了try catch?这是因为我想表达的是,如果我们在blockFun函数中一旦发生异常情况,我们这里的try catch是有能力捕获到异常的。你没有听错,就是这么神奇。例如我们在上述代码中,bolckFun函数中传入的是fales,便会throw出Exception,此异常首先会被blockDo函数的try catch捕获,然后通过 continuation.resumeWith(Result.failure(Exception())) 返回了异常,最后就是它外层的try catch捕获异常。这样异常处理起来就非常方便了,如果我们这情况下没有使用协程而是线程的话,就会稍微麻烦一点,我们只能在线程里面进行try catch了。
而往后我们需要进行其它逻辑的协程调用时,只需要对blockFun函数和自定义的ParameterContext变更即可。
13.4 小结
到这我们已经完成了Kotlin语言级别的关于协程语法的学习了,相信你一定已经解开最开始的疑惑,协程到底是什么,它是如果实现异步逻辑等等。虽然此刻你心里可能还想着协程还是没啥用,是吗?其实我在学习协程过程中也是跟你一样,因为首先对新知识的未深入了解,其次也关系着人们心里对老知识思维的巩固转变不过来。当然如果单单从这两篇文章的Demo中看确定是没啥用的,而且还不如用回我们熟悉的接口回调实现方便简单呢,但是文章开始也说的Demo只是入门语法,距离正式使用还有很长的距离。后面我们将会开始对协程官方框架的学习和使用,只有到那时你才会明白协程它是如何好用的一个东西,让我们拭目以待吧。
未完,请关注后面文章更新…