简单聊聊kotlin中的协程

初学协程,首先概念上就很难理解,光是理解挂起、恢复就能让人从入门到放弃。其实协程把这两个概念搞懂了就一通百通了,搞不懂也没关系,本篇文章的目的就是帮着大家理解这两个概念,同时花点篇幅介绍下协程里面的切换线程。

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篇博客的计划完成,哈哈,完成作业。