Android開發者快速上手Kotlin(六) 之 協程上下文和攔截器

《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是一樣的。請往下看新增的代碼。

  1. 新增了一個線程池對象singleThreadExecutor,異步邏輯就是通過它來完成的;
  2. 新增了一個AsyncContext類,上面我們瞭解到,繼承AbstractCoroutineContextElement是爲了自定義Context,繼承ContinuationInterceptor是爲了對Continuation進行攔截,裏面的interceptContinuation函數就是接收原來的Continuation,然後返回一個新的Continuation。
  3. 新增ThreadPoolContinuation類,它就是新返回的Continuation,它在resumeWith函數中使用了線程池來完成工作邏輯。

 

12.3 Demo3

我們來思考一下,是不是可以將上面新增的線程池、AsyncContext類和ThreadPoolContinuation類進行一下封裝,然後在所有的異常邏輯中進行復用,那麼往後我們要實現一些耗時操作時,只需要寫出一個普通的函數blockFun就可以了?沒錯的,所以我們的代碼又可以進一點進化成這樣:

fun main() {
    log("Main函數開始")
    coroutineDo(ParameterContext(true)) {
        try {
            val result = blockDo {
                val isSuccess = this[ParameterContext]!!.isSuccess // 通過this獲取外部傳入的context元素
                blockFun(isSuccess)
            }
            log("異步方法返回結果:${result}")
            result
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    log("Main函數結束")
}

// 使用擴展函數的形式,使block帶this對象,這裏也可以在括號里加 coroutineContext: CoroutineContext 參數把Context帶出去
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只是入門語法,距離正式使用還有很長的距離。後面我們將會開始對協程官方框架的學習和使用,只有到那時你纔會明白協程它是如何好用的一個東西,讓我們拭目以待吧。

 

 

未完,請關注後面文章更新

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章