1.协程简介
协程在Kotlin中是一个很重要的概念,也是比较难理解的概念之一。那么协程到底是怎样的存在,那么接下来让我们好好地理一理。
根据官方文档的说法,它大概有一些特性:
1.协程是轻量级的线程,一个线程中可以同时起成百上千的协程,而不会导致资源过度占用,造成系统崩溃。
2.协程运行在线程中,协程之于线程有点类似与线程之于进程,线程需要运行在进程中,同样的协程的运行也需要具体的线程来调度。线程的运行耗时的操作会阻塞,但是协程不会,它只会挂起和恢复。
3.协程可以运行在不同的线程中,由线程调度器去切换协程运行的上下文即可。
从上面的描述,我们很容易总结协程的几个特点:轻量、高效、灵活。但这些不足以让协程成为Kotlin中最亮的崽,它的高超之处在于将异步的处理简单化,用同步的语法实现异步的逻辑,避免了多线程编程中遭遇的回调地狱。
2.协程异步处理
多线程异步逻辑 VS 协成异步逻辑
下面以获取联系人电话列表为例说明两者差异
使用多线程异步处理
//getPhones by callback
getContacts(object : SuccessCallback {
override fun onCallback(result: String) {
getPhoneList(result, object : SuccessCallback {
override fun onCallback(result: String) {
print( "callback, phones result: $result \n")
}
})
}
})
fun getContacts(callback: SuccessCallback){
Thread {
run {
Thread.sleep(2000)
val users = "Bob, Anni, Jacky"
callback.onCallback(users)
}
}.start()
}
fun getPhoneList(users:String, callback: SuccessCallback){
Thread {
run {
Thread.sleep(2000)
val phones = "Bob:1213, Anni:121234, Jacky:12123"
callback.onCallback(phones)
}
}.start()
}
interface SuccessCallback{
fun onCallback(result:String)
}
使用协程异步处理
//getPhones by Coroutines
runBlocking {
val users = getContacts()
log(users)
val phones = getPhones(users)
log(phones)
print( "coroutines, phone result: $phones \n")
}
suspend fun getContacts():String{
return withContext(Dispatchers.IO){
delay(2000)
log("getContacts invoke")
"Bob, Anni, Jacky"
}
}
fun log(msg:String){
print("coroutines, $msg current thread: ${Thread.currentThread().name}\n")
}
suspend fun getPhones(users: String):String{
return withContext(Dispatchers.IO){
delay(2000)
log("getPhones invoke")
"Bob:1234, Anni:12123, Jacky:121213"
}
}
从上面的示例代码得出如下结论:
1.多线程处理异步避免不了回调嵌套,协程处理异步则可以用同步方式实现;
2.协程借助withContext之类的操作灵活地切换线程,而多线程则需要借助Handler之类的切换线程;
多线程方式还是仅包含 onSuccess 的情况,实际情况会更复杂,因为我们还要处理异常,处理重试,处理线程调度,甚至还可能涉及多线程同步。而协程在处理异步任务时就显得舒服多了。
3.协成关键点
从上面的示例代码我们不难发现协程的一些关键点
(1)协程中调用的是使用suspend修饰的挂起函数;
(2) 挂起函数不会阻塞调用线程,可以暂停和恢复。
val users = getContacts()
log(users)
val phones = getPhones(users)
log(phones)
在上面使用协程获取联系人电话列表过程中有以下几点说明:
- 表面上看起来是同步的代码,实际上也涉及到了线程切换。
- 一行代码,切换了两个线程。
- =左边:主线程
- =右边:IO线程
- 每一次从主线程到IO线程,都是一次协程挂起(suspend)
- 每一次从IO线程到主线程,都是一次协程恢复(resume)。
- 挂起和恢复,这是挂起函数特有的能力,普通函数是不具备的。
- 挂起,只是将程序执行流程转移到了其他线程,主线程并未被阻塞。
如果以上代码运行在 Android 系统,我们的 App 是仍然可以响应用户的操作的,主线程并不繁忙,这也很容易理解。
4.协程的本质
通过反编译kotlin代码编译后的.class文件,我们不难看出协程的本质
以 getContacts()
函数的反编译结果为例,原始定义为suspend fun getContacts():String
挂起函数最终转换成下面形式,suspend去掉了,但是多个Continuation
参数
public final Object getContacts(@NotNull Continuation $completion)
看看Continuation
的定义,它是一个接口,这不正是我们异步处理的常规操作吗,定义一个Callback返回结果执行结果。
public interface Continuation<in T> {
public val context: CoroutineContext
// 相当于 onSuccess 结果
// ↓ ↓
public fun resumeWith(result: Result<T>)
}
以上这个从挂起函数转换成CallBack 函数的过程,被称为:CPS 转换(Continuation-Passing-Style Transformation)。
协程以同步的方式返回执行结果又是如何实现的呢?
通过阅读反编译后的源码我们不难发现,这个过程是使用状态机完成的。
那么协程的本质可以概括为以下两点:
1.CPS转换将挂起函数转成Callback回调;
2.使用状态机完成挂起与恢复的功能机制。
5.协程作用域
协程需要运行在特定的作用域中,即CoroutineScope
, 在Android架构组件中存在各种作用域,ViewModel中使用ViewModelScope,在Activity/Fragment中使用LifecycleScope等。