初学协程,首先概念上就很难理解,光是理解挂起、恢复就能让人从入门到放弃。其实协程把这两个概念搞懂了就一通百通了,搞不懂也没关系,本篇文章的目的就是帮着大家理解这两个概念,同时花点篇幅介绍下协程里面的切换线程。
1.挂起和恢复
在学习协程的时候看过很多技术博客都有说过这么一句话:协程能够解决嵌套回调的问题。的确,这是协程的一大优势。
比如有个需求,先获取用户id,再使用用户id置换另外一个id,然后用置换后的id来获取用户信息。
不使用协程:
getUserId { userId -> getTransforId(userId) { transforId -> getUserInfo(transforId) { //获取到用户信息 } } }
使用协程:
val scope = CoroutineScope(Dispatchers.Main) scope.launch { val userId = getUserId() val transforId = getTransforId(userId) val userInfo = getUserInfo(transforId) println(userId) }
两者写法虽然在代码量上并无明显差异,但是协程的写法在逻辑上更简洁了,没有那么多回调嵌套,按顺序执行代码,最终拿到结果,很清晰。
其实google引入协程本意是为了方便开发者写线程切换的代码,但是大家这么多年以来都习惯了写线程,写回调,一时半会消化不了这个概念。啥是挂起?啥是恢复?搞得很蒙圈。
上面代码只是简单的示例,就拿getUserId()
来说,当执行到这句话的时候程序就会挂起。
挂起?首先得知道这个方法是如何实现的。
suspend fun getUserId() { println("请求到了数据") }
初学协程,很多人误以为只要方法前面加个suspend修饰就是挂起函数了,这可不对,不信你试试看:

编译器告诉你这个关键字是多余的,说白了这是个假的挂起函数。那要咋写呢?
可以这样写
suspend fun getUserId(): String { delay(1000) return "userId" }
挂起函数里面必须得有一个挂起函数,上面delay
就是一个挂起函数。
还可以这样写
suspend fun getUserId() = suspendCoroutine { continuation -> thread { //请求网络数据 //...... //回调数据 continuation.resume("userId") } }
getUserId()
方法在编译成字节码的时候,编译器会给这个方法加上Continuation
类型的参数(有一个高级的术语叫CPS,Continuation Passing Style ),suspendCoroutine
函数的作用就是拿到这个Continuation
参数,方便后续的恢复执行——也就是调用continuation.resume(xx)
后恢复到刚刚挂起的地方,然后程序继续执行val userId = getUserId()
后面那一句val transforId = getTransforId(userId)
。
综上,要实现挂起,挂起函数里面得真正的做切换线程的动作,否则无意义。
好了,挂起恢复就说完了。
什么?你在逗我吗?
真没逗你,如果不深究挂起恢复的原理,上面就基本把挂起恢复讲完了。
不对,你等等,你说了这些我理解,但是我还是不知道项目中怎么用啊。
别急,我来给你举个例子。
interface Api { @GET() suspend fun getUserInfo(@Query(value = "userId", encoded = true) userId:String) : UserResponse } val scope = CoroutineScope(Dispatchers.Main) scope.launch { val create = Retrofit.create(Api::class.java) val result = create.getUserInfo("xxx") //更新页面信息等 println(result) }
上面是我们熟悉的Retrofit
,使用协程包装了下调用过程。getUserInfo()
调用的时候Retrofit
框架会帮我们实现相应的协程相关的代码,也就是说框架里面会切线程去做网络请求,然后调用Continuation
相关resume
或者resumeWith
方法恢复执行。恢复到哪里去了?当然是把结果带出来给result
变量,然后程序继续执行后面的语句了。
如果要深入了解挂起恢复的原理,可以参考这篇文章,作者的kotlin系列文章是我见过最全最易懂的。
2.线程切换
还是借上面Retrofit
的例子
val scope = CoroutineScope(Dispatchers.Main) scope.launch { val create = Retrofit.create(Api::class.java) val result = create.getUserInfo("xxx") //更新页面信息等 println(result) }
可以在CoroutineScope(Dispatchers.Main)
构造方法中指定要切换的线程,也可以在launch(Dispatchers.IO)
中切换线程。
但这里有个地方要注意,起初在使用协程的时候,我是这样写的:
val scope = CoroutineScope(Dispatchers.IO) scope.launch { val create = Retrofit.create(Api::class.java) val result = create.getUserInfo("xxx") println("Thread:${Thread.currentThread().name}") //更新页面信息等 liveData.postValue(result) }
大家先看看有啥问题?
其实上面的写法也不能说错,代码跑起来也没问题,只是资源有些浪费,因为执行到getUserInfo("xxx")
时Retrofit
已经切换了线程了,没必要在外面在套个IO线程了。
关于Dispatchers
的原理,还是这个作者写的这篇文章,非常好。
以上,通过简短的篇幅介绍了
挂起:实际上就是切线程。
恢复:就是通过Continuation
来恢复执行挂起点后面的代码。
切换线程:实际上是让挂起点后面的代码在切换后的线程中执行。
至此,今年5篇博客的计划完成,哈哈,完成作业。