Kotlin协程深入了解一波

362 阅读4分钟

Kotlin协程深入了解一波

1.协程简介

2023-02-20 19-56-32屏幕截图.png

线程与协程关系

协程在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等。


欢迎关注我的公众号“虎哥LoveDroid”,原创技术文章第一时间推送。

Agongzhonghao.jpg

本文使用 文章同步助手 同步