扔物线--Kotlin协程训练营2期-2
笔记仅做自己学习用,方便自己复习知识。若正好可以帮助到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 = 0var respo1: String? = nullvar respo2: String? = nullapi.listRepos("bluerheaven").enqueue(object : Callback<List<Repo>?> {override fun onFailure(call: Call<List<Repo>?>, t: Throwable) {name = name or 0b1000callback.onError(t.toString())}override fun onResponse(call: Call<List<Repo>?>, response: Response<List<Repo>?>) {respo1 = response.body()?.get(0)?.namename = name or 0b0001if (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 0b1000callback.onError(t.toString())}override fun onResponse(call: Call<List<Repo>?>, response: Response<List<Repo>?>) {respo2 = response.body()?.get(0)?.namename = name or 0b0010if (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) elseStandaloneCoroutine(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函数:
@InternalCoroutinesApipublic 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 stackinline fun resumeCancellable(value: T) {if (dispatcher.isDispatchNeeded(context)) {_state = valueresumeMode = MODE_CANCELLABLEdispatcher.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()性能更好吗?并没有
那它为什么不卡线程?它只是不卡当前线程,而去卡了别的线程。
扔物线--Kotlin协程训练营2期-2相关推荐
- 扔物线--Kotlin协程训练营2期-1
笔记仅做自己学习用,方便自己复习知识.若正好可以帮助到Viewer,万分欣喜~ 若博客侵权,扔物线大大不允许放上面,麻烦告知 本文是扔物线Kotlin第二期协程训练营的第一篇文章 目录 一.Kotli ...
- 扔物线Kotlin讲解学习(一)----Kotlin的权限修饰符详解
Kotlin 中有四种可见性修饰符: public:公开,可见性最大,哪里都可以引用. private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见. protected:保护,相当于 ...
- 扔物线第一次公开演讲 Kotlin 的协程,就在这周六
今天发这篇文,是有三件重要的事要大家讲: 之前转发过北京 GDG 的一篇文章,很多人应该都知道,本周六在北京有一场 Kotlin / everywhere 的技术大会,是由北京 GDG 社区与 Kot ...
- 扔物线 298 元协程集训课程,限时免费送!
Kotlin 的协程从一推出就受到大量的关注,国内外很多团队纷纷尝试,但真正能完全正确地使用协程的人并不多,甚至有很多人和团队到现在还学不会怎么使用协程. 然而,协程却是每个已经在使用 Kotlin ...
- 谷歌开发者大会扔物线演讲原稿整理:Jetpack Compose
大家好,我是扔物线朱凯.前两天,我在 GDG DevFest 2020 的 Android Day 做了一次面向全国 Android 工程师的技术分享直播,主题是 Android 最新的 UI 框架 ...
- Kotlin协程简介
1. 什么是协程 关于协程的定义有很多,在Kotlin语言中,协程比较合理的定义应该是一个线程框架(扔物线)或者说是一种并发设计模式(官方).它是由官方设计的一套API方便开发者进行多线程开发. 2 ...
- Kotlin 协程是个什么东西?
相关阅读:一个90后员工猝死的全过程 协程是什么 根据维基百科的定义,协程(Coroutine)是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复. 协程(Coroutine) ...
- pdf 深入理解kotlin协程_Kotlin协程实现原理:挂起与恢复
今天我们来聊聊Kotlin的协程Coroutine. 如果你还没有接触过协程,推荐你先阅读这篇入门级文章What? 你还不知道Kotlin Coroutine? 如果你已经接触过协程,但对协程的原理存 ...
- Kotlin协程实现原理
前言 本篇解析Kotlin/JVM中的协程的实现原理. 初看suspend关键字 下面的例子模拟一个网络请求: class Temp {suspend fun fetchData(argument: ...
最新文章
- vue 不能监测数组长度变化length的原因
- 一个权限的设置,你会混淆么
- 2022.2.18自制玉米凉粉
- hibernate之工具类
- jvm线程分析命令_JVM:如何分析线程转储
- 【原生JS组件】javascript 运动框架
- mybatis 三级缓存查询循序_MyBatis手把手跟我做系列(五) --- 一级缓存与二级缓存
- python开发图片_python实现简单的图片隐写术
- 投资五大基本法则,助你在理财投资路上走得更平稳顺利
- Vue.js(8)- 父组件给子组件传值
- 最有效的Safari的广告拦截插件
- Python图像(字母数字)识别
- 翻译狗文档免费下载手册(补充版)
- C语言求金蝉素数,回文数 - 寂寞暴走伤的个人空间 - OSCHINA - 中文开源技术交流社区...
- 第一课 初识计算机ppt,第一课 初识Powerpoint.ppt
- 安迪的第一个字典(c++)
- revit二次开发之程序调试
- java闹钟程序_java 闹钟程序
- 用python画动态皮卡丘_如何利用python绘制可爱皮卡丘?
- 安卓神秘事件之点击事件不响应
热门文章
- 李沐windows深度学习环境配置
- 学计算机选香港哪个大学,香港知名大学计算机专业相关推荐
- BZOJ4422[Cerc2015]Cow Confinement(扫描线+线段树)
- 计算机网络技术入学要求,诺森比亚大学计算机网络技术理科硕士入学条件及实习就业...
- Centos 7.6安装LXC
- 中山大学21年计算机学院考研情况 专硕最高分442!
- 简易代理IP池的搭建
- SheetJS js-xlsx简介
- python学习笔记(十四) 邮件与短信收发
- BIM来了,结构工程师就不用画图了吧?