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

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

  • 协程与线程的关系:协程是轻量级线程

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

  • 线程运行在内核态,协程运行在用户态

    主要明白什么叫用户态,我们写的几乎所有代码,都执行在用户态,协程对于操作系统来说仅仅是第三方提供的API而已,当然运行在用户态。而线程是操作系统级别的东西,运行在内核态。

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

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

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

協程还具備以下特色:

  • 異步代碼同步化:使用編寫同步代碼的方式編寫異步代碼。

  • 輕量:您能夠在單個線程上運行多個協程,由於協程支持掛起,不會使正在運行協程的線程阻塞。掛起比阻塞節省內存,且支持多個並行操做。

  • 內存泄漏更少:使用結構化併發機制在一個做用域內執行多項操做。

  • 內置取消支持:取消操作會自動在運行中的整個協程層次結構內傳播。

  • Jetpack 集成:許多 Jetpack 庫都包含提供全面協程支持的擴展。某些庫還提供本身的協程做用域,可供您用於結構化併發。


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 顶层函数
        //启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
        //不建议使用
        //开启GlobalScope.launch这种协程之后就是在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 销毁的时候记得要取消掉,避免内存泄漏
        //不建议使用,尤其是在客户端这种需要频繁创建销毁组件的场景。
        //开启GlobalScope.launch{} 或GlobalScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是子线程)。
        mJob?.cancel()
        mJob = GlobalScope.launch(Dispatchers.Main) {
            LogManager.i(TAG, "GlobalScope thread name*****${Thread.currentThread().name}")

            showLoading()
            //进入挂起的函数后UI线程会进入阻塞状态,挂起函数执行完毕之后,UI线程再进入唤醒状态,然后往下执行
            requestUserInfo()
            //进入挂起的函数后UI线程会进入阻塞状态,挂起函数执行完毕之后,UI线程再进入唤醒状态,然后往下执行
            downloadFile()
            hideLoading()
        }


//        mJob = GlobalScope.async(Dispatchers.Main) {
//            LogManager.i(TAG, "GlobalScope thread name*****${Thread.currentThread().name}")
//
//            showLoading()
//            //进入挂起的函数后UI线程会进入阻塞状态,挂起函数执行完毕之后,UI线程再进入唤醒状态,然后往下执行
//            requestUserInfo()
//            //进入挂起的函数后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 销毁的时候记得要取消掉,避免内存泄漏
        //开启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 销毁的时候记得要取消掉,避免内存泄漏
        //开启GlobalScope.launch{} 或GlobalScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定是创建的时候的线程)。
        mMainScope = MainScope()
        mMainScope?.launch {//开启MainScope这种协程之后就是在MAIN线程执行了
            LogManager.i(TAG, "mainScope launch thread name*****${Thread.currentThread().name}")

            showLoading()
            //进入挂起的函数后UI线程会进入阻塞状态,挂起函数执行完毕之后,UI线程再进入唤醒状态,然后往下执行
            requestUserInfo()
            //进入挂起的函数后UI线程会进入阻塞状态,挂起函数执行完毕之后,UI线程再进入唤醒状态,然后往下执行
            videoDecoding()
            hideLoading()
        }

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

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


    /**
     * IO密集型协程:模拟请求用户信息(需要时间比较短的)
     */
    private suspend fun requestUserInfo() {
        LogManager.i(TAG, "start requestUserInfo")
        withContext(Dispatchers.IO) {
            delay(2 * 1000)
        }
        LogManager.i(TAG, "end requestUserInfo")
    }

    /**
     * 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密集型协程:模拟计算圆周率(需要时间比较长的)
     */
    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密集型协程:模拟视频解码(需要时间比较长的)
     */
    private suspend fun videoDecoding() {
        LogManager.i(TAG, "start videoDecoding")
        withContext(Dispatchers.Default) {
//            //这个是真正的模拟视频解码需要的时长
//            delay(20 * 60 * 1000)

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


    override fun onDestroy() {
        //这几个任务要在Activity 销毁的时候取消,避免内存泄漏
        mJob?.cancel()
        mCoroutineScope?.cancel()
        mMainScope?.cancel()
        super.onDestroy()
    }

此处之外,还有两种启动方式

  • 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{} 方法的时候可以指定运行线程(根据指定的线程来,不指定是创建的时候的线程)。
        lifecycleScope.launch {
            LogManager.i(TAG, "lifecycleScope launch thread name*****${Thread.currentThread().name}")

            showLoading()
            //进入挂起的函数后UI线程会进入阻塞状态,挂起函数执行完毕之后,UI线程再进入唤醒状态,然后往下执行
            requestUserInfo()
            //进入挂起的函数后UI线程会进入阻塞状态,挂起函数执行完毕之后,UI线程再进入唤醒状态,然后往下执行
            videoDecoding()
            hideLoading()
        }

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

 以上的协程都是串行执行的协程(遇到withContext函数就会串行执行)。

3.并行执行的协程。

先在Activity启动一个协程lifecycleScope.launch{} ,然后在协程内部启动几个launch{} 和async{},打印日志,经分析发现是并行执行的协程。

lifecycleScope.launch {
            LogManager.i(TAG, "lifecycleScope.launch thread name*****" + Thread.currentThread().name)
            launch {
                delay(1000)
                LogManager.i(TAG, "launch delay(1000)")
            }
            async {
                delay(2000)
                LogManager.i(TAG, "async delay(2000)")
            }
            async {
                delay(1000)
                LogManager.i(TAG, "async delay(1000)")
            }
            launch {
                delay(2000)
                LogManager.i(TAG, "launch delay(2000)")
            }
        }

下面是打印的日志

2023-07-03 12:07:22.202 26682-26682 ProjectFragment         com.phone.rxjava2andretrofit2        I  lifecycleScope.launch thread name*****main
2023-07-03 12:07:23.204 26682-26682 ProjectFragment         com.phone.rxjava2andretrofit2        I  launch delay(1000)
2023-07-03 12:07:23.204 26682-26682 ProjectFragment         com.phone.rxjava2andretrofit2        I  async delay(1000)
2023-07-03 12:07:24.206 26682-26682 ProjectFragment         com.phone.rxjava2andretrofit2        I  async delay(2000)
2023-07-03 12:07:24.206 26682-26682 ProjectFragment         com.phone.rxjava2andretrofit2        I  launch delay(2000)

4.在协程内交叉执行串行与并行协程,分析他们之间的关系。

先在Activity启动一个协程lifecycleScope.launch{} ,然后在协程内部交叉启动几个withContext、launch{} 和async{}方法,打印日志,经分析发现:

协程内部开启多个withContext、launch{}和async{}的时候,有一个规律,执行第一个withContext的时候就会串行执行(遇到withContext函数就会串行执行),
然后执行完了第一个withContext,再并行执行第一个launch、第一个async、第二个launch、第二个async、第二个withContext,执行第二个withContext
的时候又会串行执行(遇到withContext函数就会串行执行),然后执行完了第二个withContext,再并行执行第三个async、第四个async、第三个launch、第四个launch、第三个withContext,
执行第三个withContext的时候又会串行执行(遇到withContext函数就会串行执行),然后执行完了第三个withContext,再执行第五个launch和第五个async。
lifecycleScope.launch {
            LogManager.i(TAG, "viewModelScope.launch thread name*****" + Thread.currentThread().name)
            //协程内部开启多个withContext、launch{}和async{}的时候,有一个规律,执行第一个withContext的时候就会串行执行(遇到withContext函数就会串行执行),
            //然后执行完了第一个withContext,再并行执行第一个launch、第一个async、第二个launch、第二个async、第二个withContext,执行第二个withContext
            //的时候又会串行执行(遇到withContext函数就会串行执行),然后执行完了第二个withContext,再并行执行第三个async、第四个async、第三个launch、第四个launch、第三个withContext,
            //执行第三个withContext的时候又会串行执行(遇到withContext函数就会串行执行),然后执行完了第三个withContext,再执行第五个launch和第五个async。
            withContext(Dispatchers.IO) {//第一个withContext
                delay(2000)
                LogManager.i(TAG, "first withContext delay(2000)")
            }
            launch {//第一个launch
                delay(1000)
                LogManager.i(TAG, "first launch delay(1000)")
            }
            async {//第一个async
                delay(1000)
                LogManager.i(TAG, "first async delay(1000)")
            }
            launch {//第二个launch
                delay(1000)
                LogManager.i(TAG, "second launch delay(1000)")
            }
            async {//第二个async
                delay(1000)
                LogManager.i(TAG, "second async delay(1000)")
            }
            withContext(Dispatchers.IO) {//第二个withContext
                delay(2000)
                LogManager.i(TAG, "second withContext delay(2000)")
            }
            async {//第三个async
                delay(2000)
                LogManager.i(TAG, "third async delay(2000)")
            }
            async {//第四个async
                delay(2000)
                LogManager.i(TAG, "fourth async delay(2000)")
            }
            launch {//第三个launch
                delay(2000)
                LogManager.i(TAG, "third launch delay(2000)")
            }
            launch {//第四个launch
                delay(2000)
                LogManager.i(TAG, "fourth launch delay(2000)")
            }
            withContext(Dispatchers.IO) {//第三个withContext
                delay(1000)
                LogManager.i(TAG, "third withContext delay(1000)")
            }
            launch {//第五个launch
                delay(1000)
                LogManager.i(TAG, "fifth launch delay(1000)")
            }
            async {//第五个async
                delay(1000)
                LogManager.i(TAG, "fifth async delay(1000)")
            }
        }

下面是打印的日志

2023-07-03 12:10:09.812 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  viewModelScope.launch thread name*****main
2023-07-03 12:10:11.813 27141-27417 ResourceFragment        com.phone.rxjava2andretrofit2        I  first withContext delay(2000)
2023-07-03 12:10:12.815 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  first launch delay(1000)
2023-07-03 12:10:12.815 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  first async delay(1000)
2023-07-03 12:10:12.816 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  second launch delay(1000)
2023-07-03 12:10:12.816 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  second async delay(1000)
2023-07-03 12:10:13.820 27141-27417 ResourceFragment        com.phone.rxjava2andretrofit2        I  second withContext delay(2000)
2023-07-03 12:10:14.824 27141-27417 ResourceFragment        com.phone.rxjava2andretrofit2        I  third withContext delay(1000)
2023-07-03 12:10:15.822 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  third async delay(2000)
2023-07-03 12:10:15.823 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  fourth async delay(2000)
2023-07-03 12:10:15.824 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  third launch delay(2000)
2023-07-03 12:10:15.824 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  fourth launch delay(2000)
2023-07-03 12:10:15.824 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  fifth launch delay(1000)
2023-07-03 12:10:15.825 27141-27141 ResourceFragment        com.phone.rxjava2andretrofit2        I  fifth async delay(1000)

5.协程的好处。

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

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

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

异步代码&回调地狱

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

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

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

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

地狱到天堂:协程

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

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

使用协程的要点

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

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

挂起函数

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

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

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

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

19bd60443e76410a9c5b9e9fe3cdc72d-00003.jpg?auth_key=4841810770-0-0-9cbcddd9e77040ce56cfe2e419d18fc7

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

猜你喜欢

转载自blog.csdn.net/NakajimaFN/article/details/131493588