笔记仅做自己学习用,方便自己复习知识。若正好可以帮助到Viewer,万分欣喜~
若博客侵权,扔物线大大不允许放上面,麻烦告知
本文是扔物线Kotlin第二期协程训练营的第二篇文章
没看过第一篇文章的可以先看第一篇:https://blog.csdn.net/bluerheaven/article/details/106969835
目录
一、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()性能更好吗?并没有
那它为什么不卡线程?它只是不卡当前线程,而去卡了别的线程。