概述
在Kotlin
中,协程是一套由Kotlin官方提供的线程API。
它有优点:
- 可以通过看似同步的方式写出异步代码。这也是协程的特性之一 非阻塞式挂起。
- 可以解决地狱回调的问题。
例如,如下代码形式
launch({
val user = api.getUser() // 👈 网络请求(IO 线程)
nameTv.text = user.name // 👈 更新 UI(主线程)
})
协程的使用
在需要切换线程或者指定线程的时候 用到协程。
协程的启动
最基本用法。launch
函数加上实现在 {}
中具体的逻辑,就构成了一个协程。
有以下3种启动方式
runBlocking
启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T
,就是你协程体中最后一行是什么类型,最终返回的是什么类型T
就是什么类型。很少用,一般用于测试协程。scope.launch()
启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope
)中才能调用,返回值是一个Job
。
它具体的含义是:我要创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓「协程」是谁?就是你传给 launch 的那些代码,这一段连续代码叫做一个「协程」。scope.async()
启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope
)中才能调用。以Deferred
对象的形式返回协程任务。返回值泛型T
同runBlocking
类似都是协程体最后一行的类型。
返回的都是 Coroutine.不同点:async 返回的 Coroutine 多实现了 Deferred 接口。它的意思就是延迟,也就是结果稍后才能拿到。我们调用 Deferred.await() 就可以得到结果了。
协程的作用域 CoroutineScope
协程作用域(Coroutine Scope
)是协程运行的作用范围。launch
、async
都是CoroutineScope
的扩展函数,CoroutineScope
定义了新启动的协程作用范围,同时会继承了他的coroutineContext
自动传播其所有的 elements
和取消操作。
协程的调度器 CoroutineDispatcher
Default
:默认调度器,CPU密集型任务调度器,适合处理后台计算。通常处理一些单纯的计算任务,或者执行时间较短任务。比如:Json的解析,数据计算等IO
:IO调度器,,IO密集型任务调度器,适合执行IO相关操作。比如:网络处理,数据库操作,文件操作等Main
:UI调度器, 即在主线程上执行,通常用于UI交互,刷新等Unconfined
:非受限调度器,又或者称为“无所谓”调度器,不要求协程执行在特定线程上。
在作用域中切换线程。官方为我们提供了一个withContext
顶级函数,使用withContex
t函数来改变协程的上下文,而仍然驻留在相同的协程中,同时withContext还携带有一个泛型T返回值。
withContext
使用场景演示
// 第一种写法
coroutineScope.launch(Dispatchers.IO) {
...
launch(Dispatchers.Main){
...
launch(Dispatchers.IO) {
...
launch(Dispatchers.Main) {
...
}
}
}
}
// 通过第二种写法来实现相同的逻辑
coroutineScope.launch(Dispatchers.Main) {
...
withContext(Dispatchers.IO) {
...
}
...
withContext(Dispatchers.IO) {
...
}
...
}
async
使用场景演示
coroutineScope.launch(Dispatchers.Main) {
// 👇 async 函数启动新的协程
val avatar: Deferred = async { api.getAvatar(user) } // 获取用户头像
val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
// 👇 👇 获取返回值
show(avatar.await(), logo.await()) // 更新 UI
}
协程上下文 CoroutineContext
CoroutineContext
即协程上下文。它是一个包含了用户定义的一些各种不同元素的Element
对象集合。其中主要元素是Job
、协程调度器CoroutineDispatcher
、还有包含协程异常CoroutineExceptionHandler
、拦截器ContinuationInterceptor
、协程名CoroutineName
等。这些数据都是和协程密切相关的,每一个Element
都一个唯一key
。
CoroutineContext
有以下主要方法
public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else context.fold(this) { ...}
public fun minusKey(key: Key<*>): CoroutineContext
plus
方法, plus
有个关键字operator
表示这是一个运算符重载的方法,类似List.plus
的运算符,可以通过+
号来返回一个包含原始集合和第二个操作数中的元素的结果。同理CoroutineContext
中是通过plus
来返回一个由原始的Element
集合和通过+
号引入的Element
产生新的Element
集合。
get
方法,顾名思义。可以通过 key
来获取一个Element
fold
方法它和集合中的fold
是一样的,用来遍历当前协程上下文中的Element
集合。
minusKey
方法与plus
作用相反,它相当于是做减法,是用来取出除key
以外的当前协程上下文其他Element
,返回的就是不包含key
的协程上下文。
我们就是通过key
从协程上下文中获取我们想要的Element
,同时也解释为什么Job
、CoroutineDispatcher
、CoroutineExceptionHandler
、ContinuationInterceptor
、CoroutineName
等等,这些Element
都有需要有一个CoroutineContext.Key
类型的伴生对象key。
但是因为这个+
运算符是不对称的,所以在我们实际的运用过程中,通过+增加Element
的时候一定要注意它们结合的顺序。新增加同一类型会覆盖之前的。
//异常处理
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} :$throwable")
}
//自定义作用域
val commentJob = Job()
val commentScope = CoroutineScope(Dispatchers.Main + commentJob + exceptionHander)
//simpleRequestData 为 `CoroutineScope`的扩展函数
commentScope.simpleRequestData<ArrayList<CollectionPointModel>>()
android中的使用
GlobalScope.launch
如果不指定Dispatchers
的话,默认执行在Default
(一个计算线程).不过,作为全局Scope,执行任务是不可以取消的(不可以调用scope.cancel()
否则会抛出严重的异常)。MainScope
特点SupervisorJob()
+Dispatchers.Main
- android
ViewModule
下的viewModelScope
,默认执行在主线程
activity
fragment
的lifecycleScope
跟生命周期关联。lifecycle
状态处于DESTROYED
状态的时候自动关闭所有的协程。
同时我们也可以通过launchWhenCreated
、launchWhenStarted
、launchWhenResumed
来启动协程,等到lifecycle
处于对应状态时自动触发此处创建的协程。- 自定义
CoroutineScope
. 例如val normalScope = ContextScope(SupervisorJob() + Dispatchers.Main)
操作函数
launch
和 async
是CoroutineScope
下的函数,不可以单独使用。
coroutineScope
和 withContext
可单独使用。
launch
launch
常用函数,创建一个coroutine
并不阻塞当前线程,返回。
//launch函数的定义,
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
async
async
创建一个coroutine,返回值可立即获得,拿到这个返回值需要等任务执行完。
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
coroutineScope
coroutineScope
创建一个coroutine
,由制定的scope
执行,这个scope同外部scope,也就是调用该函数外部scope
.
常用于定义协程函数。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
使用
private suspend fun handleException(
block: suspend CoroutineScope.() -> Unit,
error: suspend CoroutineScope.(ResponseThrowable) -> Unit,
complete: suspend CoroutineScope.() -> Unit
) {
coroutineScope {
try {
block()
} catch (e: Throwable) {
//异常的处理逻辑
error(ExceptionHandle.handleException(e))
} finally {
complete()
}
}
}
withContext
withContext
由制定的coroutine
执行,挂起,结束返回。
常用于内部切换执行线程。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
关于挂起
suspend
关键字理解。它其实是一个提醒。函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。
挂起函数在执行完成之后,协程会重新切回它原先的线程。
再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作。