[架構基本功]kotlin協程的協議改造

兩年前,到微信面試的時候,人家問我懂不懂協程,知不知道里面原理,我當時懵B。一年前,去另一家公司面試的時候,人家也是這樣問我kotlin協程會用嗎?我也是沒法回答。 若是沒有實踐過,估計也沒法說出個因此來,由於壓根不知道究竟他怎麼用,使用的時候須要注意什麼。 終於近來有一個節點,本身能夠去接觸協程了,須要寫出對接口使用協程的方式擴展。java

###1.協程 不少人都會講進程,線程,協程來討論。 其實就簡單的說一下我所理解的吧 進程能夠有多個線程,線程能夠有多個協程,使用協程其實仍是線程切換。 使用協程一定要有做用域(Scope),有一個全局的做用域GlobalScope是供App內全局使用的。 而後須要一個標識上下文的context 說一下協程的優點 1.無需系統內核的上下文切換,減少開銷; 2.無需原子操做鎖定及同步的開銷,不用擔憂資源共享的問題; 3.單線程便可實現高併發,單核 CPU 即使支持上萬的協程都不是問題,因此很適合用於高併發處理,尤爲是在應用在網絡爬蟲中;web

須要注意的地方: GlobalScope調用withContext時沒法使用Dispatch.Default,由於默認是一個EmptyCoroutineContext,協程不會運行,而且不會有任何報錯。面試

###2.請求 Continuation 你們能夠考慮一個場景,當網絡協議發送後,經過非堵塞的機制來等待協議結果返回,在不改動原有的協議的使用狀況下,加入協程去改造協議處理。 初學者,其實很容易會想到使用一個協程去完成發送,而後使用另一個協程來完成接收,這樣作就能夠簡單完成操做。 那麼有沒更優美的編寫呢,先給你們看一個簡單的代碼 安全

Continuation.png
Continuation.resume.png
能夠看到經過suspendCoroutine能夠建立一個continuation對象,此對象是用於協程結果回調。當suspendCorountine執行完成後,rsp會堵塞等待continuation返回結果,再次執行。代碼是堵塞的,然而線程並無堵塞。只要對應返回的地方使用continuation來完成回調。 這裏模擬使用handler.sendMessage來模擬發送,而後經過handler.handleMessage來模擬接收。只會產生一個協程對象,任何的協議結果處理後經過continuation.resume就能夠返回成功的結果到rsp去。

###3.suspend 泛型 內聯 須要注意的是,協程域裏面,所有都須要聲明爲suspend fun的形式,提示是協程的方法,程序執行掛起的時候估計是須要特殊的標記。 協程編寫泛型的形式和java差距不是很大,可是須要注意的是,使用了協程包含了泛型對象,使用is判斷,會提示你,須要使用內聯。 bash

inline.png
使用內聯那麼私有變量所有都須要變爲public, 而T會被轉變爲 reified T。其實到編譯階段內聯的T泛型是類型是肯定的,編譯系統會將其替換掉。 並且有使用內聯,那麼方法沒法聲明爲接口方法,產生很大的侷限。 基原本說咱們肯定類型,直接強轉T就能夠了。

###4.廣播 channel received 標記 協議也並不是只有請求接收,特別若是是使用socket,那麼你確定是能有接收廣播的狀況。而上面使用協程continuation只能模擬出請求和接收的狀況,那是否有辦法接收些成廣播呢? 這裏可使用微信

private val channel = Channel<Ent>()
    fun send() {
        async {
            withContext(coroutineContext) {
                val obj = ChildEnt()
                obj.name = "協程廣播"
                obj.count = 4
                channel.send(obj)
  //            channel.close()
            }
        }
    }

    fun <T : Ent> register(callback: CoroutinesCallback<T>): Job {
        return async {
            withContext(coroutineContext) {
                for (ent in channel) {
                    callback.block.invoke(ent as T)
                }
            }
        }
    }
複製代碼

這裏須要使用channel,使用一個協程來作發送,另一個協程須要來接收。 若是你使用channel.receive()只能接收到一條數據,這裏使用,in channel的方式能夠一直監聽到channel.send的數據。 固然若是肯定通道不可用,要使用channel.close關閉通道。網絡

###5.java調用協程 若是你使用java的代碼,你會發現沒法使用協程,沒法使用域聲明。 那怎麼怎麼才能調用協程? java中仍是能聲明域對象以及CoroutineContext上下文對象的,那麼只能傳輸做用域,context,以及使用的回調的方法來作處理。併發

class CoroutinesCallback<T : IEnt>(
        var scope: CoroutineScope,
        var context: CoroutineContext,
        var block: (suspend (T) -> Unit),
        var error: (suspend (Exception) -> Unit)? = null
)
override fun <T : IEnt> sendAsCoroutineAsync(
            rspClass: Class<T>,
            scope: CoroutineScope,
            context: CoroutineContext,
            s: (T) -> Unit,
            e: ((Exception) -> Unit)?
    ): Deferred<Unit?>? {
        return sendAsCoroutineAsync(rspClass,
                        CoroutinesCallback(scope, context, {
                            s.invoke(it)
                        }, {
                            e?.invoke(it)
                        }))
   }
複製代碼

java傳輸這些能夠聲明的對象,再經過kotlin轉包一層。那爲什麼不讓外層直接傳入一個CoroutinesCallback回調對象就能夠呢? java是沒法辦法初始化suspend的初始方法的,這就很是尷尬了。折中的方法,只能使用suspend block的再包一層普通block的方法,而普通block s: (T) -> Unit能夠對應java中的Function1<T, Unit> s的方法。socket

###6.協程的回收 固然是須要考慮協程的回收的,特別在外Activity生命週期結束後,纔到達協程結果返回,若是你只是封裝消息外拋或者不在主線程還好,否則就頗有可能形成崩潰了。 協程域使用async的方法會傳回一個Deffered的對象,和Job相似,能夠經過這個對象cancel的方法能夠完成釋放,本身挑選時機就好。async

想要更加智能,參照rxjava的處理,是須要綁定lifecycle,改造的時候也是這樣作的。新版本的lifecycle加入了對協程的支持,直接是有lifecycle CoroutineScope,執行的時候,直接使用這個域就很是安全了。舊版的lifecycle並無,那這時候綁定釋放就只能本身編寫了。

class LifecycleCoroutineListener(
        private val job: Job, private val cancelEvent: Lifecycle.Event =
                Lifecycle.Event.ON_DESTROY
) : LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun pause() = handleEvent(Lifecycle.Event.ON_PAUSE)

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun stop() = handleEvent(Lifecycle.Event.ON_STOP)

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun destroy() = handleEvent(Lifecycle.Event.ON_DESTROY)

    private fun handleEvent(e: Lifecycle.Event) {
        if (e == cancelEvent && !job.isCancelled){
            job.cancel()
        }
    }
}

//使用的時候參數傳入lifecycle,而後完成綁定
lifecycle?.addObserver(LifecycleCoroutineListener(j))
複製代碼

這裏還有優化的地方,協程域上下文CoroutineContext是帶有isActive的方法的。經過封裝extendsion的方法,來對Continuation回調時先對存活判斷

private fun <T> Continuation<T>.resumeIfActive(value: T) {
        if (this.context.isActive) {
            resume(value)
        }
    }
複製代碼

###7.協程的異常處理 上面介紹了continuation的對象,使用resume能夠返回結果到掛起的等待的地方,若是失敗了的狀況,能夠放回resumeWithException的方法來返回Exception內容到接收處,可是這裏須要try catch來得到Exception。

###8.線程池問題 協程自身也是會開通線程池的,若是原本就有rxjava的一套代碼,無疑會增長線程數量的。有沒很好的方法規避呢,能夠選擇和rxjava公用線程池。

object XXDispatchers {

    /**
     * 後臺任務分發器, 使用的線程池與 Schedulers.computation() 同樣
     */
    @JvmStatic
    val Default: CoroutineDispatcher = Schedulers.computation().asCoroutineDispatcher()

    /**
     * 主線程
     */
    @JvmStatic
    val Main: CoroutineDispatcher = Dispatchers.Main

    /**
     * 協程掛起後恢復回到的線程, 與最後掛起函數運行時所在的線程相同. 即與 Dispatchers.Unconfined 相同
     */
    @JvmStatic
    val Unconfined: CoroutineDispatcher = Dispatchers.Unconfined

    /**
     * IO任務分發器, 使用的線程池與 Schedulers.io() 同樣
     */
    @JvmStatic
    val IO: CoroutineDispatcher = Schedulers.io().asCoroutineDispatcher()
}

複製代碼

最後的提醒,使用協程必定是須要做用域和上下文的,而且要考慮釋放等問題。暫時並無像rxjava同樣鏈式調用那麼方便 若是有更優化的方案,能夠再評論區評論,我會認真跟進。

兩個羣號均可以加入,羣2羣號763094035,我在這裏期待大家的加入!!!

image

羣1號是316556016。

image
相關文章
相關標籤/搜索