前言
协程是Kotlin中比较难的概念,也是其语言最重要的特性。协程这个概念并非是Kotlin首创,它在好久以前就被提出来了,但兴起却是近十年来,尤其是python、go等语言都支持协程,而且小弟认为Kotlin最被值得推崇作为Android开发的首选语言也是因为其支持协程。首次学习协程肯定会比较费力,就好像我们第一次接触线程的概念也是比较难懂的,所以我们先从理论学习,再慢慢深入到应用,然后多写多用。来吧,协程!
一、协程的基本概念
协程是什么
官方对于协程的定义是这样的:
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
简单总结以下几点:
- 协程是可以由程序自行控制挂起、恢复的程序(Kotlin协程中最关键的就是挂起函数,因为协程挂起的时候不会阻塞其他线程)
- 协程可以用来实现多任务的协作执行
- 协程可以用来解决异步任务控制流的灵活转移,解决掉繁杂的回调嵌套,让程序结构简化,可读性大大提高
协程的作用
- 协程可以降低异步程序的设计复杂度,需要注意的是协程不能让代码异步,只能让异步代码更简单
- 挂起和恢复可以控制执行流程的转移(比如网络请求时可以从UI线程切换到IO线程,执行完回调结果又可以切换回UI线程)
- 异步逻辑可以用同步代码的形式写出
- 同步代码比异步代码更灵活,更容易实现复杂业务
线程 VS 协程
线程(Thread)是指操作系统的线程,被称为内核线程,线程是抢占式运行,也就是一个线程时间到了,另外一个线程就会接上运行,另外线程切换或线程阻塞的开销都比较大;协程(Coroutine)则是指语言实现的协程,其主要讲究的是协作,运行在内核线程之上的,也就是协程依赖于线程,所以协程可以看做是一个非常轻量级的线程。协程的挂起是非阻塞的,开发者可以自由的控制协程的挂起和恢复,一个线程中可以创建任意数量的协程。
小结:协程可以将嵌套繁杂的异步编程简化成顺序表达,代码的可读性大大提高,并且协程了提供协程挂起方法,这是一种基本无代价且可控的操作去替代了阻塞线程的方法。
二、协程的基本要素
挂起函数
什么是挂起函数?
以suspend
关键字修饰的函数就是挂起函数,而所谓的挂起就是切换线程后会被自动切回来的线程调度操作。
挂起函数作用域
挂起函数只能在其他挂起函数或者协程中调用。
挂起函数的挂起和恢复
- 挂起函数调用时包含了协程挂起的语义
- 挂起函数返回时则包含了协程恢复的语义
挂起函数的类型
挂起函数的类型和普通函数类型的区别只在于加上suspend
关键字。例如:
suspend fun foo(){}
对应类型是: suspend () -> Unit
suspend fun foo(a:Int):String{ TODO() }
对应类型是: suspend (Int) -> String
挂起函数是如何做到挂起恢复的?
挂起函数的的挂起和恢复其实是通过Continuation
这个接口来实现的,我们定义的
suspend fun foo(){}
方法实际上的是这样的:
fun foo(continuation: Continuation<Unit>): Any{ TODO()}
但这个Continuation
是编译器帮我们传的,所以在我们定义的函数中是隐藏的,所以挂起函数只能在其他挂起函数或者协程中调用,因为只有在这两情况里面才有这个Continuation
。
下面我们用Retrofit
和GitHub开放的api是实现一下如何将异步回调写成挂起函数,这里主要是分为三步:
- 使用
suspendCoroutine
获取挂起函数的Continuation
- 请求成功回调的分支使用
Continuation.resume(value)
- 请求失败回调使用
Continuation.resumeWithException(t)
首先对Retrofit初始化和写请求接口方法:
val githubServieApi by lazy {
val retrofit = retrofit2.Retrofit.Builder()
.client(OkHttpClient.Builder().addInterceptor {
it.proceed(it.request()).apply {
println("request: ${code()}")
}
}.build())
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(GitHubApi::class.java)
}
interface GitHubApi {
@GET("users/{login}")
fun getUserCallback(@Path("login") login: String): Call<User>
}
data class User(val id: String, val name: String, val url: String)
改造异步回调成为挂起函数
suspend fun getUserSuspend(name: String) = suspendCoroutine<User> { continuation ->
//enqueue的调用会把我们的操作切换到IO线程,所以真正的挂起必须是异步调用resume,包括切换到其他线程resume或者是单线程事件循环异步执行
//如果直接调用resume不算挂起的
githubServieApi.getUserCallback(name).enqueue(object: Callback<User> {
override fun onFailure(call: Call<User>, t: Throwable) =
continuation.resumeWithException(t)
override fun onResponse(call: Call<User>, response: Response<User>) =
response.takeIf { it.isSuccessful }?.body()?.let(continuation::resume)
?: continuation.resumeWithException(HttpException(response))
})
}
suspend fun main(){
val user = getUserSuspend("Jeremyzwc") //这里在idea会看到有一个箭头的模样,那这里就是一个挂起点
println(user)
}
打印结果:
request: 200
User(id=20693153, name=Max_z, url=https://api.github.com/users/Jeremyzwc)
二、协程的创建
- 协程是一段可执行的程序
- 协程的创建通常需要一个函数 (suspend function)
- 协程的创建也需要一个API (createCoroutine)
创建协程主要通过Continuation
的createCoroutine
方法,这里提供了带receiver
和不带的两种方法:
public fun <T> (suspend () -> T).createCoroutine(
completion: Continuation<T>
): Continuation<Unit>
public fun <R, T> (suspend R.() -> T).createCoroutine(
receiver: R,
completion: Continuation<T>
): Continuation<Unit>
这里要注意的是一个协程需要两个Continuation
,其中一个是suspend
函数本身执行需要一个Continuation
实例在恢复时调用,即上面函数中completion
,这个是传进来的,另外一个Continuation
是返回值Continuation<Unit>
,这是创建出来的协程的载体,receiver suspend
函数会被传给该实例作为协程的实际执行体。
三、协程的启动
启动协程通过Continuation
的startCoroutine
方法,这里一样提供了带receiver
和不带的两种启动方式:
public fun <T> (suspend () -> T).startCoroutine(
completion: Continuation<T>
) {
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}
public fun <R, T> (suspend R.() -> T).startCoroutine(
receiver: R,
completion: Continuation<T>
) {
createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)
}
对于使用createCoroutine
方法创建协程只要调用resume
方法就可以启动协程:
suspend {
}.createCoroutine(object :Continuation<Unit>{
override val context: CoroutineContext
get() = TODO("Not yet implemented")
override fun resumeWith(result: Result<Unit>) {
TODO("Not yet implemented")
}
}).resume(Unit)
如果使用startCoroutine
方法则使用suspend
函数直接调用就启动了协程,startCoroutine
方法包含了协程的创建和启动:
suspend {
}.startCoroutine(object :Continuation<Unit>{
override val context: CoroutineContext
get() = TODO("Not yet implemented")
override fun resumeWith(result: Result<Unit>) {
// 协程执行完调用
println("Coroutinue end with $result")
}
})
四、协程上下文
- 协程执行过程中需要携带数据
- 索引是CoroutineContextKey
- 元素是CoroutineContextElement
CoroutineContext
主要是作为数据的载体,对数据进行包装和存储。在协程中CoroutineContext
会携带协程的名字、异常处理器等。
五、拦截器:ContinuationInterceptor
-
ContinuationInterceptor
是协程上下文里面的一个元素:
interface ContinuationInterceptor : CoroutineContext.Element
- 可以对协程上下文所在协程的
Continuation
进行拦截
interface ContinuationInterceptor : CoroutineContext.Element {
fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
}
拦截Continuation
最主要的就是切换线程,上面的interceptContinuation
方法可以到传进去了一个Continuation<T>
又返回了一个Continuation<T>
,这个有点类似于Okhttp里面的拦截器中拦截request,返回response的操作。
总结一下Continuation
的执行流程:
一般情况下我们会通过以下方式创建并启动一个协程:
suspend {
...
}.createCoroutine(...)
suspend {...}
是协程的本体,也是协程逻辑执行的地方,它执行创建完之后会创建出一个SuspendLambda
的类,这个类是Continuation
的实现类,在标准库ContinuationImpl
中。
如果suspend {...}
中还有挂起函数的执行,那SuspendLambda
会被SafeContinuation
包装,也可以认为SafeContinuation
是一个拦截器,对SuspendLambda
进行拦截,这个SafeContinuation
的作用主要是确保以下两点:
- resume只被调用一次
- 如果在当前线程调用栈上直接调用则不会挂起,所以真正的挂起一定要切线程的
类似下面这种情况:
suspend {
//SafeContinuation仅在挂起点时出现
a()
}.createCoroutine(...)
suspend fun a() = suspendCoroutine<Unit> {
thread {
//这里的resume实际调用的是SafeContinuation的resume
it.resume(Unit)
}
}
这里SafeContinuation
的resume
执行完之后会恢复到suspend {...}
的执行,所以最后也会调用到SuspendLambda
的resume
方法。
小结:
本篇文章主要介绍和学习Kotlin协程的基本概念和基本要素,了解Kotlin实现协程的流程。基本概念包括:
- 协程的定义:挂起和恢复
- 协程的作用:控制流程转移和异步逻辑同步化
- 和线程的对比
基本要素主要是包括:
Continuation
- 挂起函数(挂起函数类型和异步回调改写)
- 协程的创建
- 协程上下文(拦截器的作用)
下一篇文章我们将记录学习如何使用官方协程框架来实现协程。