Kotlin Coroutine协程
- Kotlin Coroutine 协程是什么? Coroutine协程这个概念在20世纪60年代就有了,可谓久远,Wiki 百科上也有解释。有人说它是控制流的让出和恢复,也有说能像线程并发处理但不会阻塞,官方说它比线程更轻量化。我们并不需要把Kotlin Coroutine神化,它究竟是什么?
通过最近一年多阅读文档、使用及阅读源码的感受:
- 运行在线程里,实际是运行在线程池里。
- 因为运行在线程里,只要使用不当,依然会存在阻塞的情况,例如使用
sleep()
,或者死锁。 - 让协程与
new Thread()
新建线程的方式来比较性能消耗,来显得更轻量化是不厚道的。我觉得应该与Executors.newCachedThreadPool()
来比才合适。 - 让编写异步代码变得容易,特别是在一个多个异步同时处理的时候。
- 异步代码简单了之后,我们可以把UI线程解脱出来,使用更多异步风格,优化了UI性能。
- 在UI线程和IO线程切换十分的方便。
- 少了回调及广播的方式来处理异步,代码更容易阅读。
第一个Kotlin Coroutine
实现一个从网络请求用户信息,并且把用户昵称显示在UI上的功能: 实现第一个
GlobalScope.launch(Dispatchers.Main) {
val userInfo = getUserFromNetwork(userId)//网络请求,运行在后台
textView.text = userInfo.name//更新UI,运行在主线程
}
suspend fun getUserFromNetwork(userId: String): String {
... //具体这里的实现忽略,下文会分析到
}
从上面的协程示例中看到:
- 网络请求与UI写在一个方法序列里,没有回调
- 2行逻辑是运行在不同的线程里的
getUserFromNetwork()
有suspend
这个修饰符GlobalScope.launch
使用了这个方法来启动一个协程
回调的困境
例如需要在一个列表中显示,用户的在线状态及等级。
//需要通过网络请求后台api
api.getUserInfo(userId)//1.查询用户信息
api.getOnlineStatus(userId)//2.查询在线状态
api.getLevelInfo(userId)//3.查询等级
3个api之间没有依赖关系,最合理的方式应该是3个一起并发请求再组装数据。
但在用回调时,要实现这样的逻辑会变得很困难,权衡之下会把它写成了在回调中串行执行,如此整个网络的延时就会是原来的3倍。
回调的串行实现
api.getUserInfo(userId, object : Callback<UserInfo> {
override fun onResponse(response: Response<UserInfo>) {
val userInfo = response.body
api.getOnlineStatus(userId, object: Callback<OnlineStatus> {
override fun onResponse(response: Response<OnlineStatus>) {
val onlineStatus = response.body
api.getLevelInfo(userId, object: Callback<LevelInfo> {
override fun onResponse(response: Response<LevelInfo>) {
val levelInfo = response.body
val composeInfo = compose(userInfo, onlineStatus, levelInfo)//组装数据
getLiveData().postValue(composeInfo)//刷新UI
}
}
}
}
})
Coroutine的并行实现
实现上述的业务逻辑,协程是怎么做的?
GlobalScope.launch {
//async里的3个block是同时请求,无需等待前一个的结果
val userInfo = async { api.getUserInfo(userId) }
val onlineStatus = async { api.getOnlineStatus(userId) }
val levelInfo = async { api.getLevelInfo(userId) }
//等待3个请求全部返回了,再组装数据
val composeInfo = compose(userInfo.await(), onlineStatus.await(), levelInfo.await())
getLiveData().postValue(composeInfo)//刷新UI
}
我们可以用顺序的方式来让多线程执行起来,在同步和异步之间灵活的切换。这样的特性,可以让我们写出之前很难才能做出的逻辑,这是Coroutine的优势。
- 使用
async
来启动一个新的协程@userInfo,返回一个Deferred
,调用Deferred.await()
方法,此时当前协程会挂起,等待@userInfo执行结束。
runBlocking 上面已经讲了2种启动协程的方式,分别是
launch
,async
。然而还有一种叫runBlocking
的方式,在官方文档也有写到,同时也发现了会有同学对这个方式存在一些使用上的误解情况。 当运行到这个runBlocking()
的时候当前线程会被阻塞住。特别注意这个方法不应在Coroutine内部使用。根据官方文档说明,它是被用在阻塞main
线程及测试的时候使用的。不建议大家使用。
runBlocking {
api.getUserFromNetwork(userId)
api.getOnlineStatus(userId)
}
...//直到getUserFromNetwork运行完才会被执行到
在Coroutine里运行在不同的线程上
这是一个第一个示例的全版:
GlobalScope.launch(Dispatchers.Main) {
val userInfo = getUserFromNetwork(userId)//网络请求,运行在后台
textView.text = userInfo.name//更新UI,运行在主线程
}
suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
HttpSerivce.getUser(userId)
}
withContext
Coroutine里有一个withContext()
的函数,它可以指定协程在哪个线程里执行,并让后续代码等待,按顺序去执行。
有了这个函数,可以消除在切换线程时导致的Callback嵌套。
如果我们通过启动不同的协程来切换线程,代码是长这样的:
GlobalScope.launch(Dispatchers.IO) {
...
launch(Dispatchers.Main) {
...
launch(Dispatchers.IO) {
...
}
}
}
是不是又有一种回调的感觉回来了
而使用withContext()
则可以让协程摆脱上面的嵌套写法。
GlobalScope.launch(Dispatchers.Main) {
val result0 = withContext(Dispatchers.IO) {...}
val result1 = withContext(Dispatchers.Main) {...}
val result2 = withContext(Dispatchers.IO) {...}
}
这里需要跟前面并发请求的情况区分开来,使用的场景不同进行选择。
suspend(挂起)
GlobalScope.launch(Dispatchers.Main) {
val userInfo = getUserFromNetwork(userId)//网络请求,运行在后台
textView.text = userInfo.name//更新UI,运行在主线程
}
suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
HttpSerivce.getUser(userId)
}
可见,对于suspendGetUserInfo()
内的逻辑运行在什么线程里,可以不由调用者决定的,可以由实现者决定的。
我们去设计自己的挂起函数时,如果需要在特定的线程里,最好的方式是我们函数内部去指定。比如操作文件读写的逻辑时,定义运行在
Dispatchers.IO
里,这样也不用担心外面会使用错误。
何为挂起?!
- 既不是函数被挂起,也不是线程被挂起,而是当前协程被挂起,正在运行这个协程的线程,从挂起的那时候开始,不再执行这个协程了。
- 协程执行到挂起函数的地方时,就会脱离运行它的线程,这条线程并不会阻塞,它会去干别的事情。
- 协程脱离后,并不是指它停止了,而是等待被系统安排其它的线程在适当的时机来运行它。
launch(Dispatchers.Main) {
val userInfo = suspendGetUserInfo(userId)
textView.text = userInfo.name
}
suspend fun suspendGetUserInfo(userId: Long): UserInfo {
//可切换线程IO线程执行,原来执行的Main线程将空闲执行其它工作
return withContext(Dispatchers.IO) {
getUserFromNetwork(userId)
}
}
//在Main UI线程执行
log.debug("run in click starting")
GlobalScope.launch(Dispatchers.Main) {
log.debug("run in launch")
val userInfo = suspendGetUserInfo(userId)
}
log.debug("run in click finishing")
打印的顺序是?
运行结果
D: 11:50:30.825 main: run in click starting
D: 11:50:30.869 main: run in click finishing
D: 11:50:30.872 main: run in launch
D: 11:50:30.873 main: run in suspendGetUserInfo starting
协程运行的时候,尽管还是在Main里运行,实际上也是在下个一个Main Looper时才运行到。
suspend fun suspendGetUserInfo(userId: Long): UserInfo {
//可切换线程IO线程执行,原来执行的Main线程将空闲执行其它工作
log.debug("run in suspendGetUserInfo starting")
GlobalScope.launch(Dispatchers.Main) {
log.debug("Main is available")
}
val userInfo = withContext(Dispatchers.IO) {
delay(100)
log.debug("run in withContext")
getUserFromNetwork(userId)
}
log.debug("run in suspendGetUserInfo finishing")
return userInfo
}
打印的顺序是?
D: 12:03:06.469 main : run in suspendGetUserInfo starting
D: 12:03:06.475 main : Main is available
D: 12:03:06.582 DefaultDispatcher-worker-2 : run in withContext
D: 12:03:06.585 main : run in suspendGetUserInfo finishing
由于
withContext
是挂起函数,已经切换到IO上执行,因此Main是空闲的,下一个Looper的时候就可以执行launch
里的代码。
suspend的语法规则
- Kotlin协程规定,一个
suspend
方法的调用者必须是suspend
方法或者是在launch()/async()/runBlocking()
启动的协程调用。 - 协程的基础库有不少带有
suspend
的函数,当我们要去使用时,要留意符合上面的规则。反之,没有用到suspend
函数的地方,并不需要给自己函数加上,Android Studio也会相应的代码提示。
其它挂起函数
除了withContext()
,还有
- 像上面所用过的
await()
delay()
表示挂起一定时间后再运行,协程里记得不要用sleep()
withTimeout { }
表示block执行如果超过一定的时间则会抛出TimeoutCancellationException
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
public suspend fun delay(timeMillis: Long)
如何将Callback代码改为Coroutine
之前我们开发的过程中如果使用了Callback的形式来处理异步逻辑时,此时我们想把Callback的API改为Coroutine要怎么实现呢?
suspendCancellableCoroutine/suspendCoroutine
//原来的Callback形式方法
fun saveToRepositoryCallback(callback: (Int) -> Unit) { ... }
GlobalScope.launch(Dispatchers.IO) {
val returnValue = saveToRepository()
}
//协程的形式
suspend fun saveToRepository(): Int {
return suspendCoroutine { continuation ->
saveToRepositoryCallback {
continuation.resume(it)
}
}
}
通过一个回调的参数continuation
来设置返回的数据,此时就相当于把Callback改为了直接返回的形式。
之外如果需要Coroutine能抛出异常时,可以使用resumeWithException
来把异常抛出来。
//原来的Callback形式方法,第二个参数为当出错返回的异常回调
fun saveToRepositoryCallback(success: (Int) -> Unit, error: (Throwable) -> Unit) {...}
GlobalScope.launch(Dispatchers.IO) {
try {
val returnValue = saveToRepository()
} catch (t: Throwable) {
// do something on error
}
}
//调用该方法有可能会throw exception
suspend fun saveToRepository(): Int {
return suspendCoroutine { continuation ->
saveToRepositoryCallback({
continuation.resume(it)
}, {
continuation.resumeWithException(it)
})
}
}
suspendCancellableCoroutine特性
使用suspendCoroutine
启动的Job
,想通过cancel()
来停止是不行的,依然会继续执行
val job = GlobalScope.launch(Dispatchers.IO) {
val returnValue = saveToRepository()
LogUtil.debug("returnValue: $returnValue")
}
//协程的形式
suspend fun saveToRepository(): Int {
return suspendCoroutine { continuation ->
saveToRepositoryCallback {
LogUtil.debug("callback: $it")
continuation.resume(it)
}
}
}
//触发取消
job.cancel()
I: [main] callback: 0
I: [DefaultDispatcher-worker-2] returnValue: 0
如果使用suspendCancellableCoroutine
,运行returnValue
的代码则不会被运行到
val job = GlobalScope.launch(Dispatchers.IO) {
val returnValue = saveToRepository()
LogUtil.debug("returnValue: $returnValue")
}
suspend fun saveToRepository(): Int {
return suspendCancellableCoroutine { continuation ->
saveToRepositoryCallback {
LogUtil.debug("callback: $it")
continuation.resume(it)
}
}
}
I: [main] callback: 0
总结
对于Kotlin Coroutine来说,更像是一个跨线程工具。可以把它看成是类似于AsyncTask,Exeutors,Handler,Rxjava之类的工具库。 碰到下面的情况时建议使用协程:
- 有多个并发的任务同时进行,或者想通过并发提高性能的时候
- 需要在UI线程和工作线程里做切换的时候
思考
- 协程能做到完全避免阻塞问题吗?
- 比用线程轻量化吗?
附录
如何添加Kotlin Coroutine的依赖:在build.gradle
中添加依赖库。
buildscript {
ext {
kotlin_version = '1.3.50'
coroutines_android_version = '1.3.2'
}
}
dependencies {
//依赖kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//这是协程android的库,同时也依赖了kotlin coroutine库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"
}