kotlin协程的详细介绍和六种启动方式与挂起函数原理

1.首先我们来了解一下什么是协程?

(1)协程与线程的关系:协程是轻量级线程

可以说,协程不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以恢复继续运行。所以说,协程和线程相比并不是一个维度的概念。函数调用当然比线程切换轻量;而封装成类线程的API后,它形似线程(可手动启动、有各种运行状态、能够协作工作、能够并发执行)。协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。

协程比线程更高效,可以挂起和恢复而不会产生上下文切换的开销;所以挂起比阻塞节省内存,而不会导致相同的性能损失。这句话是什么意思呢?我们都知道,线程是操作系统管理的,而协程不是被操作系统内核所管理,而完全是由开发者所管理(控制),这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。稍微总结下,大致就三个特点:

协程是轻量级的,创建一个线程栈大概需要1M左右,而一个协程栈大概只需要几K或者几十K

减少了线程切换的成本,协程可以挂起和恢复,它不会产生额外的开销,由开发者所管理(控制)

不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多

(2)线程运行在内核态,协程运行在用户态

主要明白什么叫用户态,就是由开发者所管理(控制),协程对于操作系统来说仅仅是第三方提供的API而已,当然由开发者所管理(控制)。而线程是操作系统级别的东西,运行在内核态。

(3)协程是一个线程框架(扔物线表述)

从包含关系上看,协程跟线程的关系,有点像“线程与进程的关系”,毕竟,协程不可能脱离线程运行。有一点必须明确的是,一个线程的多个协程的运行是串行的,如果是多核CPU,多个线程是可以并行运行的,但是一个线程内协程却绝对是串行的,无论CPU有多少个核。毕竟协程虽然是一个特殊的函数,但仍然是一个函数,一个线程内可以运行多个函数,但这些函数都是串行运行的,当一个线程内的一个协程运行时,其它协程必须挂起。

当然了,如果是多核CPU,多个线程内的多个协程(每个线程对应一个协程的情况下)是可以并行执行的。

协程虽然不能脱离线程而运行,但可以在不同的线程之间切换。看到这,大家应该能理解本文最开始放的那张动图的含义了吧?


2.协程的使用。

(1)首先引入协程依赖

在module 下的build.gradle文件下的dependencies标签下加入依赖

kapt "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
kapt "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"

(2)启动协程有六种方式

  • runBlocking{}
    方法一:使用 runBlocking 顶层函数(不建议使用)
            启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
            开启 runBlocking{} 这种协程之后就是在MAIN线程执行了

    private fun startRunBlocking() {
        //方法一:使用 runBlocking 顶层函数(不建议使用)
        //启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
        //开启 runBlocking{} 这种协程之后就是在MAIN线程执行了
        runBlocking {
            LogManager.i(TAG, "runBlocking thread name*****${Thread.currentThread().name}")

            requestUserInfo()
            downloadFile()
        }
    }

  • GlobalScope.launch{} 或 GlobalScope.async{}
    方法二:使用GlobalScope 单例对象直接调用launch/async开启协程不建议使用
            适合在应用范围内启动一个新协程,协程的生命周期与应用程序一致。
            如果在Activity/Fragment启动,即使Activity/Fragment已经被销毁,协程仍然在执行,极限情况下可能导致资源耗尽,所以需要绑定生命周期(就是在Activity/Fragment 销毁的时候取消这个协程),避免内存泄漏。

        不建议使用,尤其是在客户端这种需要频繁创建销毁组件的场景。
        开启GlobalScope.launch{} 或GlobalScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是子线程)。

var mJob: Job? = null

private fun startGlobalScope() {
        //方法二:使用GlobalScope 单例对象直接调用launch/async开启协程(不建议使用)
        //适合在应用范围内启动一个新协程,协程的生命周期与应用程序一致。
        //如果在Activity/Fragment启动,即使Activity/Fragment已经被销毁,协程仍然在执行,极限情况下可能导致资源耗尽,所以需要绑定生命周期(就是在Activity/Fragment 销毁的时候取消这个协程),避免内存泄漏。
        //不建议使用,尤其是在客户端这种需要频繁创建销毁组件的场景。
        //开启GlobalScope.launch{} 或GlobalScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是子线程)。
        mJob?.cancel()
        mJob = GlobalScope.launch(Dispatchers.Main) {
            LogManager.i(TAG, "GlobalScope thread name*****${Thread.currentThread().name}")

            showLoading()
            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
            requestUserInfo()
            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
            downloadFile()
            hideLoading()
        }


//        mJob = GlobalScope.async(Dispatchers.Main) {
//            LogManager.i(TAG, "GlobalScope thread name*****${Thread.currentThread().name}")
//
//            showLoading()
//            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
//            requestUserInfo()
//            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
//            downloadFile()
//            hideLoading()
//        }
    }

  • 实现CoroutineScope + launch{} 或 CoroutineScope + async{}
    方法三:创建一个CoroutineScope 对象,创建的时候可以指定运行线程(默认运行在子线程)
            即使Activity/Fragment已经被销毁,协程仍然在执行,所以需要绑定生命周期(就是在Activity/Fragment 销毁的时候取消这个协程),避免内存泄漏。
            开启mCoroutineScope?.launch{} 或mCoroutineScope?.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定是创建的时候的线程)。

var mCoroutineScope: CoroutineScope? = null

private fun startCoroutineScope() {
        //方法三:创建一个CoroutineScope 对象,创建的时候可以指定运行线程(默认运行在子线程)
        //即使Activity/Fragment已经被销毁,协程仍然在执行,所以需要绑定生命周期(就是在Activity/Fragment 销毁的时候取消这个协程),避免内存泄漏。
        //开启mCoroutineScope?.launch{} 或mCoroutineScope?.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定是创建的时候的线程)。
        mCoroutineScope = CoroutineScope(Dispatchers.Main)
        mCoroutineScope?.launch {
            LogManager.i(TAG, "mCoroutineScope thread name*****${Thread.currentThread().name}")
            showLoading()
            calculatePi()
            hideLoading()
        }


//        mCoroutineScope?.async {
//            LogManager.i(TAG, "mCoroutineScope2 async thread name*****${Thread.currentThread().name}")
//            requestUserInfo()
//        }
//        mCoroutineScope?.async {
//            LogManager.i(TAG, "mCoroutineScope3 async thread name*****${Thread.currentThread().name}")
//            videoDecoding()
//        }
    }

  • MainScope+launch{} 或 MainScope+async{}
    方法四:创建一个MainScope 对象,默认运行在UI线程
            即使Activity/Fragment已经被销毁,协程仍然在执行,所以需要绑定生命周期(就是在Activity/Fragment 销毁的时候取消这个协程),避免内存泄漏。
            开启mMainScope?.launch{} 或 mMainScope?.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定是创建的时候的线程)。


var mMainScope: CoroutineScope? = null
    
private fun startMainScope() {
        //方法四:创建一个MainScope 对象,默认运行在UI线程
        //即使Activity/Fragment已经被销毁,协程仍然在执行,所以需要绑定生命周期(就是在Activity/Fragment 销毁的时候取消这个协程),避免内存泄漏。
        //开启mMainScope?.launch{} 或 mMainScope?.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定是创建的时候的线程)。
        mMainScope = MainScope()
        mMainScope?.launch {//开启MainScope这种协程之后就是在MAIN线程执行了
            LogManager.i(TAG, "mainScope launch thread name*****${Thread.currentThread().name}")

            showLoading()
            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
            requestUserInfo()
            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
            videoDecoding()
            hideLoading()
        }

//        mMainScope?.async {
//            LogManager.i(TAG, "mainScope async thread name*****${Thread.currentThread().name}")
//
//            showLoading()
//            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
//            requestUserInfo()
//            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
//            videoDecoding()
//            hideLoading()
//        }
    }

下面是协程执行的具体方法和协程取消方法,说一下关于IO密集成和CPU密集型协程的区别:
1)指定协程在Dispatchers.IO工作,IO密集型协程主要是指执行IO操作的协程:包括网络请求、数据库增删改查、文件下载等等;
2)指定协程在Dispatchers.Default工作,CPU密集型也叫计算密集型,此时,系统运作大部分的状况是CPU Loading 100%,主要是指执行大量逻辑运算的协程:包括计算圆周率、高清视频解码等等。

/**
     * IO密集型协程:模拟请求用户信息(需要时间比较短的)
     * 指定协程在Dispatchers.IO工作,IO密集型协程主要是指执行IO操作的协程:包括网络请求、数据库增删改查、文件下载等等。
     */
    private suspend fun requestUserInfo() {
        LogManager.i(TAG, "start requestUserInfo")
        withContext(Dispatchers.IO) {
            delay(2 * 1000)
        }
        LogManager.i(TAG, "end requestUserInfo")
    }

    /**
     * IO密集型协程:模拟下载文件
     * 指定协程在Dispatchers.IO工作,IO密集型协程主要是指执行IO操作的协程:包括网络请求、数据库增删改查、文件下载等等。
     */
    private suspend fun downloadFile() {
        LogManager.i(TAG, "start downloadFile")
        withContext(Dispatchers.IO) {
//            //这个是真正的模拟下载文件需要的时长
//            delay(60 * 1000)

            //这里只是想早点看到效果,所以减少了时长
            delay(5 * 1000)
        }
        LogManager.i(TAG, "end downloadFile")
    }

    /**
     * CPU密集型协程:模拟计算圆周率(需要时间比较长的)
     * 指定协程在Dispatchers.Default工作,CPU密集型也叫计算密集型,此时,系统运作大部分的状况是CPU Loading 100%,
     * 主要是指执行大量逻辑运算的协程:包括计算圆周率、高清视频解码等等。
     */
    private suspend fun calculatePi() {
        LogManager.i(TAG, "start calculatePi")
        withContext(Dispatchers.Default) {
//            //这个是真正的模拟计算圆周率需要的时长
//            delay(5 * 60 * 1000)

            //这里只是想早点看到效果,所以减少了时长
            delay(10 * 1000)
        }
        LogManager.i(TAG, "end calculatePi")
    }

    /**
     * CPU密集型协程:模拟视频解码(需要时间比较长的)
     * 指定协程在Dispatchers.Default工作,CPU密集型也叫计算密集型,此时,系统运作大部分的状况是CPU Loading 100%,
     * 主要是指执行大量逻辑运算的协程:包括计算圆周率、高清视频解码等等。
     */
    private suspend fun videoDecoding() {
        LogManager.i(TAG, "start videoDecoding")
        withContext(Dispatchers.Default) {
//            //这个是真正的模拟视频解码需要的时长
//            delay(20 * 60 * 1000)

            //这里只是想早点看到效果,所以减少了时长
            delay(10 * 1000)
        }
        LogManager.i(TAG, "end videoDecoding")
    }

此处之外,还有两种启动方式
  • viewModelScope.launch{} 或 viewModelScope.async{}

        方法五:在Android MVVM架构的ViewModel中启动一个新协程(如果你的项目架构是MVVM架构,则推荐在ViewModel中使用),该协程默认运行在UI线程,协程和ViewModel的生命周期绑定,组件销毁时,协程一并销毁,从而实现安全可靠地协程调用。
        调用viewModelScope.launch{} 或 viewModelScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是UI线程)。

fun startViewModelScope() {
        //在Android MVVM架构的viewModel中启动一个新协程(推荐使用),该协程默认运行在UI线程,协程和该组件生命周期绑定,
        //组件销毁时,协程一并销毁,从而实现安全可靠地协程调用。
        //调用viewModelScope.launch{} 或viewModelScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是UI线程)。
        viewModelScope.launch {
            LogManager.i(
                TAG,
                "viewModelScope.launch thread name*****" + Thread.currentThread().name
            )
            delay(2000)
        }

//        viewModelScope.async {
//            LogManager.i(TAG, "viewModelScope.async thread name*****" + Thread.currentThread().name)
//            delay(2000)
//        }
    }

  • lifecycleScope.launch{} 或 lifecycleScope.async{}

        方法六:在Activity/Fragment 启动一个协程,该协程默认运行在UI线程(推荐使用)
        协程和该组件生命周期绑定,Activity/Fragment销毁时,协程一并销毁,从而实现安全可靠地协程调用。
        调用lifecycleScope.launch{} 或 lifecycleScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是UI线程)。

private fun startLifecycleScope() {
        //方法六:在Activity/Fragment 启动一个协程,该协程默认运行在UI线程(推荐使用),
        //协程和该组件生命周期绑定,Activity/Fragment销毁时,协程一并销毁,从而实现安全可靠地协程调用。
        //调用lifecycleScope.launch{} 或 lifecycleScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是UI线程)。
        lifecycleScope.launch {
            LogManager.i(
                TAG,
                "lifecycleScope launch thread name*****${Thread.currentThread().name}"
            )

            showLoading()
            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
            requestUserInfo()
            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
            videoDecoding()
            hideLoading()
        }

//        lifecycleScope.async {
//            LogManager.i(TAG, "lifecycleScope async thread name*****${Thread.currentThread().name}")
//
//            showLoading()
//            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
//            requestUserInfo()
//            //进入挂起的函数后(执行withContext函数的时候)UI线程会进入挂起状态,挂起函数执行完毕之后,UI线程再进入恢复状态,然后往下执行
//            videoDecoding()
//            hideLoading()
//        }
    }

3.协程的好处。

高效和轻量,都不是 Kotlin 协程的核心竞争力。

Kotlin 协程的核心竞争力在于:它能简化异步并发任务。

作为 Java 开发者,我们很清楚线程并发是多么的危险,写出来的异步代码是多么的难以维护。

异步代码&回调地狱

使用Java 代码发起了一个异步请求,从服务端查询用户的信息,通过 CallBack 返回 response:

到目前为止,我们的代码看起来并没有什么问题,但如果我们的需求变成了这样呢?

查询用户信息 --> 查找该用户的好友列表 -->拿到好友列表后,查找该好友的动态

有点恶心了,是不是?这还是仅包含 onSuccess 的情况,实际情况会更复杂,因为我们还要处理异常,处理重试,处理线程调度,甚至还可能涉及多线程同步。

地狱到天堂:协程

今天的主角是协程,上面的代码用协程应该写?很简单,核心就是三行代码:

是不是简洁到了极致?这就是 Kotlin 协程的魅力:以同步的方式完成异步任务。

使用协程的要点

以上代码的关键,在于那三个请求函数的定义,它们都被 suspend 修饰,这代表它们都是:挂起函数。

那么,挂起函数到底是什么?

挂起函数

挂起函数(Suspending Function),从字面上理解,就是可以被挂起的函数。suspend 有:挂起,暂停的意思。在这个语境下,也有点暂停的意思。暂停更容易被理解,但挂起更准确。

挂起函数,能被「挂起」,当然也能「恢复」,他们一般是成对出现的。

我们来看看挂起函数的执行流程,注意动画当中出现的闪烁,这代表正在请求网络。

「一定要多看几遍,确保没有遗漏其中的细节。」

koltin 挂起函数的执行流程(串行执行)

从上面的动画,我们能知道:

  • 表面上看起来是同步的代码,实际上也涉及到了线程切换。

  • 一行代码,切换了两个线程。

  • =左边:主线程

  • =右边:IO线程

  • 每一次从主线程到IO线程,都是一次协程挂起(suspend)

  • 每一次从IO线程到主线程,都是一次协程恢复(resume)。

  • 挂起和恢复,这是挂起函数特有的能力,普通函数是不具备的。

  • 挂起,只是将程序执行流程转移到了其他线程,主线程并未被阻塞。

  • 如果以上代码运行在 Android 系统,我们的 App 是仍然可以响应用户的操作的,主线程并不繁忙,这也很容易理解。

挂起函数的执行流程我们已经很清楚了,那么,Kotlin 协程到底是如何做到一行代码切换两个线程的?

这一切的魔法都藏在了挂起函数的suspend关键字里。

suspend的本质

suspend 的本质,就是 CallBack。

有的小伙伴要问了,哪来的 CallBack?明明没有啊。确实,我们写出来的代码没有 CallBack,但 Kotlin 的编译器检测到 suspend 关键字修饰的函数以后,会自动将挂起函数转换成带有 CallBack 的函数。

如果我们将上面的挂起函数反编译成 Java,结果会是这样:

从反编译的结果来看,挂起函数确实变成了一个带有 CallBack 的函数,只是这个 CallBack 的真实名字叫 Continuation。毕竟,如果直接叫 CallBack 那就太 low,对吧?

我们看看 Continuation 在 Kotlin 中的定义:

对比着看看 CallBack 的定义:

从上面的定义我们能看到:Continuation 其实就是一个带有泛型参数的  CallBack,除此之外,还多了一个 CoroutineContext,它就是协程的上下文。对于熟悉 Android 开发的小伙伴来说,不就是 context 嘛!也没什么难以理解的,对吧?

以上这个从挂起函数转换成CallBack 函数的过程,被称为:CPS 转换(Continuation-Passing-Style Transformation)。

看,Kotlin 官方用 Continuation 而不用 CallBack 的原因出来了:Continuation 道出了它的实现原理。当然,为了理解挂起函数,我们用 CallBack 会更加的简明易懂。

这个转换看着简单,其中也藏着一些细节。

函数类型的变化

上面 CPS 转换过程中,函数的类型发生了变化:suspend ()->String 变成了 (Continuation)-> Any?

这意味着,如果你在 Java 访问一个 Kotlin 挂起函数getUserInfo(),在 Java 看到 getUserInfo() 的类型会是:(Continuation)-> Object。(接收 Continuation 为参数,返回值是 Object)

挂起函数

挂起函数经过 CPS 转换后,它的返回值有一个重要作用:标志该挂起函数有没有被挂起。

这听起来有点绕:挂起函数,就是可以被挂起的函数,它还能不被挂起吗?是的,挂起函数也能不被挂起。

让我们来理清几个概念:

只要有 suspend 修饰的函数,它就是挂起函数,比如我们前面的例子:

当 getUserInfo() 执行到 withContext的时候,就会返回 CoroutineSingletons.COROUTINE_SUSPENDED 表示函数被挂起了。

现在问题来了,请问下面这个函数是挂起函数吗:

答案:它是挂起函数。但它跟一般的挂起函数有个区别:它在执行的时候,并不会被挂起,因为它就是普通函数。当你写出这样的代码后,IDE 也会提示你,suspend 是多余的:

当 noSuspendFriendList() 被调用的时候,不会挂起,它会直接返回 String 类型:"Tom,Jack"。这样的挂起函数,你可以把它看作「伪挂起函数」

挂起函数小结

  • suspend 修饰的函数就是挂起函数。

  • 挂起函数,在执行的时候并不一定都会挂起。

  • 挂起函数只能在其他挂起函数中 or 协程作用域被调用。

  • 挂起函数里包含其他挂起函数的时候,它才会真正被挂起。


 

如对此有疑问,请联系qq1164688204。

推荐Android开源项目

项目功能介绍:原本是RxJava2和Retrofit2项目,现已更新使用Kotlin+RxJava2+Retrofit2+MVP架构+组件化和 Kotlin+Retrofit2+协程+MVVM架构+组件化,添加自动管理token 功能,添加RxJava2 生命周期管理,集成极光推送、阿里云Oss对象存储和高德地图定位功能。

项目地址:https://gitee.com/urasaki/RxJava2AndRetrofit2

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值