【Android】Kotlin 协程 高效并发详解( Kotlin Coroutine )

目录

  • 协程
    • 1. 协程优点
    • 2. 协程作用域
    • 3. 协程上下文
    • 4. 协程启动方式
      • (1) launch
      • (2) async
      • 5. 线程调度(切换)
      • 6. 协程挂起(阻塞)
      • 7. 实现并发
      • 8. 协程取消

        这篇内容原本是放在这篇文章里面的: Kotlin 核心语法详解(快速入门)

        但是觉得那篇内容较多,协程又相对重要,所以单拎出来。


        协程

        协程是一种编程思想,并不局限于特定的语言。除 Kotlin 以外,其他的一些语言,如 Go、Python 等都可以在语言层面上实现协程。

        Kotlin Coroutine 本质上是 Kotlin 官方提供的一套线程封装 API,其设计初衷是为了解决并发问题,让协作式多任务实现起来更方便。子任务协作运行,优雅的处理异步问题解决方案。

        协程是一种比线程更加轻量级的存在,不是线程,但是可以把它类比成线程。

        协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程运行并依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。

        协程具有以下非常重要的特点(这些都是高效运用协程的关键):

        1.协程不阻塞当前线程。

        2.两个独立协程互不干扰。

        3.父子协程互不干扰。(协程里面可以再创建协程,叫子协程)

        4.在一个协程中,必须等挂起函数执行完才能继续执行。

        1. 协程优点

        (1) 协程可以帮我们自动切线程

        (2) 摆脱了链式回调的问题

        2. 协程作用域

        CoroutineScope即协程运行的作用域,主要作用是提供CoroutineContext,即协程运行的上下文。使用协程之前一定要用到它。

        常用的 viewModelScope、LifecycleScope、MainScope、GlobalScope都是继承CoroutineScope。

        GlobalScope 是全局协程空间,启动方式简单,但是内部运行或者子协程运行报错容易造成内存泄漏。

        更推荐使用MainScope,使用的时候初始化,不用的时候销毁,cancel方法也会停止协程空间内的所有线程。

        单独的launch只能在主协程控件内运行,所以可以理解为协程的子协程

        3. 协程上下文

        协程是通过Dispatchers调度器来控制线程切换的,调度Kotlin协程在哪个线程上执行。例如:Android 中 UI 操作必须要切回主线程操作。

        Dispatchers 调度器:

        Dispatchers.Default :当没有定义调度器时的默认值。用于中央处理器密集型的工作,计算操作,算法,类似的排序和转化数据工作

        Dispatchers.IO :最常用,用于磁盘操作像是网络请求、数据库读写、文件下载。没有CPU密集操作

        Dispatchers.Main :UI线程(主线程)

        4. 协程启动方式

        runBlocking:T  //不常用,只在主线程使用,阻塞主线程,delay延迟阻塞    
        launch:Job  //创建无返回值的协程
        async:Deferred //创建有返回值的协程,用await()获取返回结果
        withContext //不创建新的协程,指定协程上运行代码块
        

        (1) launch

        创建方式:

        GlobalScope.launch { // 方法1:使用 GlobalScope 单例对象,调用 launch 开启协程
        }
         
        val coroutineScope = CoroutineScope(context)
        coroutineScope.launch { // 方法2:自行通过 CoroutineContext 创建一个 CoroutineScope 对象
        }
        

        方法 1 与 runBlocking 相比不会阻塞线程,但它的生命周期会和 APP 一致,且无法取消;

        方法 2 比较推荐使用,可以通过 context(CoroutineContext) 参数去管理和控制协程的生命周期。

        launch :启动协程,用代码测试

        println("测试是否为主线程" + (Thread.currentThread() == Looper.getMainLooper().thread))
        println("测试开始")
        launch { println("测试是否为主线程" + (Thread.currentThread() == Looper.getMainLooper().thread))
            println("测试延迟开始")
            delay(20000)
            println("测试延迟结束")
        }
        println("测试结束")
        
        17:19:17.190 System.out: 测试是否为主线程 true
        17:19:17.190 System.out: 测试开始
        17:19:17.202 System.out: 测试结束
        17:19:17.203 System.out: 测试是否为主线程 false
        17:19:17.203 System.out: 测试延迟开始
        17:19:37.223 System.out: 测试延迟结束
        

        测试的时候是主线程,但是到了 launch 中就会变成子线程,这种效果类似 new Thread(),和 runBlocking 最不同的是 launch 没有执行顺序这个概念

        (2) async

        async :异步。用代码测试

        println("测试是否为主线程" + (Thread.currentThread() == Looper.getMainLooper().thread))
        println("测试开始")
        async { println("测试是否为主线程" + (Thread.currentThread() == Looper.getMainLooper().thread))
            println("测试延迟开始")
            delay(20000)
            println("测试延迟结束")
        }
        println("测试结束")
        
        17:29:00.694 System.out: 测试是否为主线程 true
        17:29:00.694 System.out: 测试开始
        17:29:00.697 System.out: 测试结束
        17:29:00.697 System.out: 测试是否为主线程 false
        17:29:00.697 System.out: 测试延迟开始
        17:29:20.707 System.out: 测试延迟结束
        

        这结果不是跟 launch 一样么?那么这两个到底有什么区别呢?,让我们先看一段测试代码

        println("测试是否为主线程" + (Thread.currentThread() == Looper.getMainLooper().thread))
        println("测试开始")
        val async = async { println("测试是否为主线程" + (Thread.currentThread() == Looper.getMainLooper().thread))
            println("测试延迟开始")
            delay(20000)
            println("测试延迟结束")
            return@async "666666"
        }
        println("测试结束")
        runBlocking { println("测试返回值:" + async.await())
        }
        
        17:50:57.117 System.out: 测试是否为主线程 true
        17:50:57.117 System.out: 测试开始
        17:50:57.120 System.out: 测试结束
        17:50:57.120 System.out: 测试是否为主线程 false
        17:50:57.120 System.out: 测试延迟开始
        17:51:17.131 System.out: 测试延迟结束
        17:51:17.133 System.out: 测试返回值:666666
        

        这里就说明了 async 和 launch 还是有区别的,async 可以有返回值,通过它的 await 方法进行获取,需要注意的是这个方法只能在协程的操作符中才能调用

        5. 线程调度(切换)

        (开发实例:在IO线程进行网络请求数据,请求成功在UI线程更新控件的情况)

        协程也有类似 RxJava 的线程调度,可以建立一个公共的线程池 CommonPool供线程重复使用,不用一个个维护,也可以用Dispatchers指定切换到不同线程。

        (1)withContext :切换可使用顶层函数 withContext 方法,这个方法必须在挂起函数或者协程中使用,可以切换到指定的线程,并在执行结束之后,自动把线程切换回去继续执行。在协程空间内,执行1个或多个withContext是顺序同步执行的。非协程空间内,就是单独创建协程

        (2)coroutineScope+launch :或者用coroutineScope+launch(单独的launch是创建协程)切换,如下所示:

        coroutineScope.launch(Dispatchers.Main) { // 在 UI 线程开始
            val image = withContext(Dispatchers.IO) { // 切换到 IO 线程
                getImage(imageId)                        // 在 IO 线程执行
            }
            //val image= coroutineScope{ //	launch{ //		getImage(imageId)
            //	}
            //}
            imageView.setImageBitmap(image)              // 自动回到 UI 线程更新 UI
        }
        

        该方法支持自动切回原来的线程,协程能够消除并发代码在协作时产生的嵌套。如果需要频繁地进行线程切换,这种写法将有很大的优势,这就是使用同步的方式写异步代码。

        //另一种方式
        coroutineScope.launch(Dispatchers.IO) { // 可以通过 Dispatchers.IO 参数把任务切到 IO 线程执行
        }
        coroutineScope.launch(Dispatchers.Main) { // 也可以通过 Dispatchers.Main 参数切换到主线程
        }
        

        6. 协程挂起(阻塞)

        协程挂起的本质就是切线程,完成之后不用手动切换线程,可以自动切回来。用于并发,需要使用到协程特用的suspend关键字将函数标注成挂起函数。

        suspend函数:挂起函数只能在协程或者另一个挂起函数中调用。运行函数时主协程会挂起等待该函数执行完毕再切换回主协程继续执行,会阻塞当前协程,

        suspend fun rn1() : Int { delay(1000L)
            Log.e("rn1" , "调用了rn1()方法")
            return 1
        }
        suspend fun rn2() : Int { delay(2000L)
            Log.e("rn2" , "调用了rn2()方法")
            return 2
        }
        GlobalScope.launch { val time = measureTimeMillis { val n1 = rn1()
                Log.e("n1" , "需要获取n1")
                val n2 = rn2()
                Log.e("n2" , "需要获取n2")
                val result = n1 + n2
                Log.e("执行完毕" , result.toString())
            }
            Log.e("运行时间",time.toString())
        }
        打印结果:
        rn1: 调用了rn()方法
        n1: 需要获取n1
        rn2: 调用了rn2()方法
        n2: 需要获取n2
        执行完毕: 3
        运行时间: 3010
        

        7. 实现并发

        (开发实例:在IO线程进行第一次请求接口,请求成功拿到数据做参数再去请求第二个接口的情况)

        并发是几个协程一起分别执行,提高应用性能。需要用到async函数去等待多个请求的结果再去执行后续操作。

        async函数必须在协程作用域中调用,会创建一个新的子协程,await()方法也是一个挂起函数,它的作用是等待协程运行结束并获取返回结果

        GlobalScope.launch(Dispatchers.Main) { val time = measureTimeMillis { Log.e("--" , "开始运行第一个协程")
                val deferred1 = async { Log.e("--" , "子协程1运行开始")
                    rn1()
                }
                Log.e("--" , "开始运行第二个协程")
                val deferred2 = async { Log.e("--" , "子协程2运行开始")
                    rn2()
                }
                Log.e("--" , "开始计算结果")
                val result = deferred1.await() + deferred2.await()
                Log.e("执行完毕" , result.toString())
            }
            Log.e("运行时间" , time.toString())
        }
        打印结果如下:
        开始运行第一个协程
        开始运行第二个协程
        开始计算结果
        子协程1运行开始
        子协程2运行开始
        rn1: 调用了rn1()方法
        rn1: 调用了rn2()方法
        执行完毕: 3
        运行时间: 2009
        

        这里使用async开启了两个子协程,两个子协程都有挂起函数,所以两个子协程都会被挂起,但他们的父协程在调用await()挂起函数之前没有都没有被挂起,所以可以正常运行,两个子协程并发执行。

        8. 协程取消

        不是全局的协程,退出页面或者在ViewModel中必须取消,但是例如ViewModel中viewModelScope创建协程由vm管理取消,可不用手动取消。

        在协程中用 cancel() 或者 用协程返回值 job.cancel()