美文网首页Android开发Android开发经验谈Android开发
[架构基本功]kotlin协程的协议改造

[架构基本功]kotlin协程的协议改造

作者: CangWang | 来源:发表于2020-04-20 11:25 被阅读0次

    两年前,到微信面试的时候,人家问我懂不懂协程,知不知道里面原理,我当时懵B。一年前,去另外一家公司面试的时候,人家也是这样问我kotlin协程会用吗?我也是无法回答。
    如果没有实践过,估计也无法说出个所以来,因为压根不知道究竟他怎么用,使用的时候需要注意什么。
    终于近来有一个节点,自己可以去接触协程了,需要写出对接口使用协程的方式扩展。

    1.协程

    很多人都会讲进程,线程,协程来讨论。
    其实就简单的说一下我所理解的吧
    进程可以有多个线程,线程可以有多个协程,使用协程其实还是线程切换。
    使用协程必定要有作用域(Scope),有一个全局的作用域GlobalScope是供App内全局使用的。
    然后需要一个标识上下文的context
    说一下协程的优势
    1.无需系统内核的上下文切换,减小开销;
    2.无需原子操作锁定及同步的开销,不用担心资源共享的问题;
    3.单线程即可实现高并发,单核 CPU 即便支持上万的协程都不是问题,所以很适合用于高并发处理,尤其是在应用在网络爬虫中;

    需要注意的地方:
    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判断,会提示你,需要使用内联。


    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的方法。

    6.协程的回收

    当然是需要考虑协程的回收的,特别在外Activity生命周期结束后,才到达协程结果返回,如果你只是封装消息外抛或者不在主线程还好,不然就很有可能造成崩溃了。
    协程域使用async的方法会传回一个Deffered<T>的对象,和Job类似,可以通过这个对象cancel的方法可以完成释放,自己挑选时机就好。

    想要更加智能,参照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

    相关文章

      网友评论

        本文标题:[架构基本功]kotlin协程的协议改造

        本文链接:https://www.haomeiwen.com/subject/jbrzmhtx.html