扔物线--Kotlin协程训练营2期-2

2 篇文章 0 订阅
2 篇文章 0 订阅

 

笔记仅做自己学习用,方便自己复习知识。若正好可以帮助到Viewer,万分欣喜~

若博客侵权,扔物线大大不允许放上面,麻烦告知


本文是扔物线Kotlin第二期协程训练营的第二篇文章

没看过第一篇文章的可以先看第一篇:https://blog.csdn.net/bluerheaven/article/details/106969835

 

目录

一、Retrofit对协程的支持

二、Retrofit和RxJava的结合使用

三、合并网络请求

1. 自己实现合并网络请求

2. 用Rxjava实现合并网络请求

3. 用Kotlin协程实现合并网络请求

四、协程和Jetpack架构组件

1. 协程泄漏

2. Jetpack组件对协程的支持

五、协程和线程

六、本质探秘

1. 协程是怎么切线程的?

2. 协程为什么可以从主线程“挂起”,却不卡主线程?

3. 协程的delay()和Thread.sleep()


 

 

一、Retrofit对协程的支持

先来看一下Kotlin使用Retrofit的方式,不适用协程:

    @GET("users/{user}/repos")
    fun listRepos(@Path("user") user: String): Call<List<Repo>>
        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        val api = retrofit.create(Api::class.java)
        api.listRepos("rengwuxian").enqueue(object:Callback<List<Repo>?>{
            override fun onFailure(call: Call<List<Repo>?>, t: Throwable) {
            }

            override fun onResponse(call: Call<List<Repo>?>, response: Response<List<Repo>?>) {
                textView.text = response.body()?.get(0).name
            }
        })

 第一段代码定义Api.kt接口类,第二段代码,创建Retrofit对象,并动态代理给val api,通过api调用执行网络请求。

再来看看使用Kotlin协程的写法:

@GET("users/{user}/repos") suspend fun listReposKt(@Path("user") user: String): List<Repo>
        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        val api = retrofit.create(Api::class.java)
        
        GlobalScope.launch(Dispatchers.Main) {
            try {
                val repos = api.listReposKt("rengwuxian") // 后台
                textView.text = repos[0].name // 前台
            } catch (e: Exception) {
                textView.text = e.message // 出错
            } 
        }

第一段代码统一定义了接口,第二段代码同样创建了Retrofit对象,但是在使用时就不一样了。使用Kotlin协程不再需要回调,直接把前台后台代码按顺序写下来就可以了。(这里我想吐槽:onFailure用try catch替代了,瞬间感觉有点low...)

训练营的第一篇文章说过,suspend只是起到标记和提醒的作用,并不能切换线程,那么这里是怎么切线程的?答案是,Retrofit动态帮助我们创建了代理类,切换了线程。(Retrofit对Kotlin的支持还真是强大)

这里还有个坑:如果不写try catch代码,也不会报错,因为Kotlin没有checked Exception。而Java是有的。(如果在Java里写可能抛出异常的代码,而没有try catch,编译器会报错)

 

二、Retrofit和RxJava的结合使用

    @GET("users/{user}/repos")
    fun listReposRx(@Path("user") user: String): Single<List<Repo>>
        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
            .build()
        val api = retrofit.create(Api::class.java)

        api.listReposRx("rengwuxian")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(object : SingleObserver<List<Repo>?> {
                override fun onSuccess(t: List<Repo>) {
                }

                override fun onSubscribe(d: Disposable) {
                }

                override fun onError(e: Throwable) {
                }
            })

同样是先定义Api.kt接口列,再创建Retrofit实例后,通过api动态代理进行网络请求。

这里使用了RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())让网络请求在线程中执行。.observeOn(AndroidSchedulers.mainThread())又保证了结果在主线程进行处理。

这里有个快捷键的小技巧,在写sucribe里的回调时,可以使用快捷键Alt + shift + 空格调出都有哪些subscribe参数可用。我的快捷键是Eclipse的,快捷键名称是smart type。

对比第一节的Kotlib与Retrofit结合的代码,两者好像都可以...

三、合并网络请求

1. 自己实现合并网络请求

先看代码:

        var name = 0
        var respo1: String? = null
        var respo2: String? = null
        api.listRepos("bluerheaven").enqueue(object : Callback<List<Repo>?> {
            override fun onFailure(call: Call<List<Repo>?>, t: Throwable) {
                name = name or 0b1000
                callback.onError(t.toString())
            }

            override fun onResponse(call: Call<List<Repo>?>, response: Response<List<Repo>?>) {
                respo1 = response.body()?.get(0)?.name
                name = name or 0b0001
                if (name == 0b0011) {
                    callback.success("$respo1 + $respo2")
                }
            }
        })

        api.listRepos("google").enqueue(object : Callback<List<Repo>?> {
            override fun onFailure(call: Call<List<Repo>?>, t: Throwable) {
                name = name or 0b1000
                callback.onError(t.toString())
            }

            override fun onResponse(call: Call<List<Repo>?>, response: Response<List<Repo>?>) {
                respo2 = response.body()?.get(0)?.name
                name = name or 0b0010
                if (name == 0b0011) {
                    callback.success("$respo1 + $respo2")
                }
            }
        })

这里通过状态位来判断两个接口的情况,有任何一个接口失败,就回调失败。当两个接口同时成功后,回调成功。实现了合并请求。这里Retrofit保证了name的操作都是在主线程进行,如果不能保证都在主线程运行,需要加锁后再操作做。

这样的代码逻辑,虽然也能实现功能,但逻辑复杂。

先来看看RxJava的合并网络请求实现

2. 用Rxjava实现合并网络请求

熟悉RxJava的人都知道,可以用zip操作符来进行网络请求的合并,两个接口都成功后,返回结果。代码如下:

        Single.zip<List<Repo>, List<Repo>, String>(
            api.listReposRx("bluerheaven"),
            api.listReposRx("google"),
            BiFunction{repos1, repos2 -> "${repos1[0].name} - ${repos2[0].name}"}
        ).observeOn(AndroidSchedulers.mainThread())
            .subscribe(object : SingleObserver<String?> {
                override fun onSuccess(combined: String) {
                    textView.text = combined
                }

                override fun onSubscribe(d: Disposable) {
                }

                override fun onError(e: Throwable) {
                    textView.text = e.message
                }
            })

3. 用Kotlin协程实现合并网络请求

这里要使用到协程的async函数,它和launch一样,都是开启一个协程。async的好处是,我们可以在以后通过await方法来获取代码的运行结果。如果async代码执行完成后调用await获取值,就会立即得到结果,如果在async执行中调用await,就会挂起,等待async返回结果。两个async代码块在协程里,会并发执行,第二个async并不会等待第一个async执行完成。

来看看使用Kotlin实现合并网络请求的代码:

        GlobalScope.launch(Dispatchers.Main) { 
            try {
                val bluerheaven = async { api.listReposKt("bluerheaven") }
                val google = async { api.listReposKt("google") }
                textView.text = "${bluerheaven.await()[0].name} + ${google.await()[0].name}"
            }catch (e: Exception) {
                textView.text = e.message
            }
        }

是不是更简单了~~~

对比一下协程和RxJava:

1. 功能应用场景很接近

2. 协程写法更简单一点

3. Rxjava性能上要比协程好一些

四、协程和Jetpack架构组件

1. 协程泄漏

什么是协程泄漏?协程泄漏就是当你已经不需要这个协程了,但这个协程还在工作。

举个栗子,使用协程做以下工作: 从网络上取图片 -> 裁剪、放缩图片 -> 圆角图片,加边框 -> 显示

如果在第一步从网络上获取图片时,用户已经退出了Activity,那么这个协程会继续工作。这就是协程泄漏。

协程泄漏的本质还是线程泄漏。

那退出页面时,怎样取消协程呢?答案是通过协程的cancel方法取消:

        val job = GlobalScope.launch(Dispatchers.Main) {
            ...
        }
        job.cancel()

有没有办法统一取消所有的协程呢?有的,它是CoroutineScope -- “结构化并发”。其实每一个协程(包括上面的GlobalScope)都在运行一个CoroutineScope,可以理解为它是对多个协程的集中管理地。

    val scope = MainScope()
    
    init {
        scope.launch {
            // todo something
        }
        scope.launch {
            // todo something
        }
        scope.cancel()  //取消所有以scope开启的协程
    }

可以自定义一个MainScope,表示协程主要在主线程,以这个scope开启的所有协程,当调用scope.cancel时,都会统一取消。

2. Jetpack组件对协程的支持

先在app下的build.gradle中添加ktx -- KoTlin eXtention 的依赖:

    implementation 'androidx.activity:activity-ktx:1.1.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

就可以这样用:

        lifeCycleScope.launch{
        }
        lifeCycleScope.launchWhenResume{
        }
        viewModelScope.launch {
        }

通过lifeCycleScope启动协程,就不需要再onDestroy里取消,因为会自动做这件事。lifeCycleScope的launchWhenResume意思就是它本身的意思,就是在调用Activity的onResume时启动这个协程。viewModelScope同样可以启动一个协程。

五、协程和线程

1. 协程和线程分别是什么?

线程就是线程,协程是一个线程库

2. 协程和线程哪个更容易使用?

当然是协程了,你作为一个上层库,还没原型好使,那要你何用?

所以应该问:协程和Executor,哪个容易使用?

一般来说还是协程,这东西实在有点太突破了,关键就是它的“消除回调”

3. 协程相比线程的优势和劣势?

优势就是好用,强大;劣势呢?上手太难了

那么...同样问一下,和Executor相比呢,有什么劣势?

一样,难上手。没办法,它太新了。

4.  那和Handler相比呢?
首先,其实没法比,它俩也不是一个维度的东西。Handler相当于一个“只负责Android中切线程”的特殊场景化的Executor,在Android中你要想让协程切刀主线程,还是得用Handler。

如果我就是要强行对比协程和Handler,它有什么劣势?

我们要真是从易用性上面来说,你用协程来往主线程切,还真的是比直接用Handler更好些、更方便的。这个...应该也算是个比较强行的优势?

六、本质探秘

1. 协程是怎么切线程的?

这里来追一下这段源码:

        GlobalScope.launch(Dispatchers.Main) {
        }

点进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
}

进入coroutine.start

    public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        initParentJob()
        start(block, receiver, this)
    }

start其实这里是一个函数对象的调用,start并非调用了本身,而是调用了它的invoke函数,跟进CoroutineStart的三个参数的invoke函数:

    @InternalCoroutinesApi
    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
        }

这里有4个,经常用的就是CoroutineStart.DEFAULT,跟进startCoroutineCancellable:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
    }

再进入resumeCancellable

internal fun <T> Continuation<T>.resumeCancellable(value: T) = when (this) {
    is DispatchedContinuation -> resumeCancellable(value)
    else -> resume(value)
}

再进入resumeCancellable

    @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
    inline fun resumeCancellable(value: T) {
        if (dispatcher.isDispatchNeeded(context)) {
            _state = value
            resumeMode = MODE_CANCELLABLE
            dispatcher.dispatch(context, this)
        } else {
            executeUnconfined(value, MODE_CANCELLABLE) {
                if (!resumeCancelled()) {
                    resumeUndispatched(value)
                }
            }
        }
    }

这里调用了dispatcher.dispatch,但是里面是一个抽象的实现,那么它的具体实现是啥,就不追了。来看一下dispatcher的一个具体实现HandlerDispatcher的dispatch函数:

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        handler.post(block)
    }

看到这里就明白了,最终调用了handler的post函数,对这个再熟悉不过了。Java里切线程底层用的就是它。

结论:协程切线程的底层也是调用handler.post()

2. 协程为什么可以从主线程“挂起”,却不卡主线程?

因为所谓的“从主线程挂起”,其实是结束了再主线程的执行,把后面的代码放在了后台线程执行,以及在后台线程的工作做完后,再把更靠后的代码通过Handler.post()又抛回主线程。

3. 协程的delay()和Thread.sleep()

delay()性能更好吗?并没有

那它为什么不卡线程?它只是不卡当前线程,而去卡了别的线程。

 

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值