Kotlin协程实战进阶之筑基篇1

电子说

1.2w人已加入

描述

前言

公司开启新项目了,想着准备亮一手 Kotlin 协程应用到项目中去,之前有对 Kotlin 协程的知识进行一定量的学习,以为自己理解协程了,结果……实在拿不出手!

为了更好的加深记忆和理解,更全面系统深入地学习 Kotlin 协程的知识,协程将分为三部分来讲解,本文是第一篇

一、概述

协程的概念在1958年就开始出现(比线程还早), 目前很多语言开始原生支, Java 没有原生协程但是大型公司都自己或者使用第三方库来支持协程编程, 但是Kotlin原生支持协程。

Android 中的每个应用都会运行一个主线程,它主要是用来处理 UI,如果主线程上需要处理的任务太多,应用就感觉被卡主一样影响用户体验,得让那些耗时的任务不阻塞主线程的运行。要做到处理网络请求不会阻塞主线程,一个常用的做法就是使用回调,另一种是使用协程。

协程概念

很多人都会问协程是什么?这里引用官方的解释:

1.协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

2.协程是一种并发设计模式。

协程就像轻量级的线程,为什么是轻量的?因为协程是依赖于线程,一个线程中可以创建N个协程, 很重要的一点就是协程挂起时不会阻塞线程 ,几乎是无代价的。而且它 基于线程池API ,所以在处理并发任务这件事上它真的游刃有余。

协程只是一种概念,它提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法: 协程挂起和恢复本质上Kotlin协程就是作为在Kotlin语言上进行异步编程的解决方案,处理异步代码的方法

有可能有的同学问了,既然它基于线程池,那我直接使用线程池或者使用 Android 中其他的异步任务解决方案,比如 Handler、AsyncTask、RxJava等,不更好吗?

协程可以 使用阻塞的方式写出非阻塞式的代码 ,解决并发中常见的回调地狱。消除了并发任务之间的协作的难度,协程可以让我们轻松地写出复杂的并发代码。一些本来不可能实现的并发任务变的可能,甚至简单,这些才是协程的优势所在。

作用

  • 1.协程可以让异步代码同步化
  • 2.协程可以降低异步程序的设计复杂度

特点

  • 轻量 :您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  • 内存泄漏更少 :使用结构化并发机制在一个作用域内执行多项操作。
  • 内置取消支持 :取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成 :许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

Kotlin Coroutine 生态

ui

kotlin的协程实现分为了两个层次:

  • 基础设施层 :标准库的协程API,主要对协程提供了概念和语义上最基本的支持;
  • 业务框架层 kotlin.coroutines :协程的上层框架支持,基于标准库实现的封装,也是我们日常开发使用的协程扩展库。

依赖库

projectgradle 添加 Kotlin 编译插件:

dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
}

要使用协程,还需要在app的 build.gradle 文件中添加依赖:

dependencies {
     //协程标准库
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
    //协程核心库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
    //协程Android支持库,提供安卓UI调度器
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}

这里我们主要使用协程扩展库, kotlin协程标准库太过于简陋不适用于开发者使用。

二、原理

协程的概念最核心的点就是函数或者一段程序能够被挂起,稍后再在挂起的位置恢复 。协程通过主动让出运行权来实现协作,程序自己处理挂起和恢复来实现程序执行流程的协作调度。因此它本质上就是在讨论程序控制流程的机制。

使用场景

kotlin协程基于Thread相关API的封装,让我们不用过多关心线程也可以方便地写出并发操作,这就是Kotlin的协程。协程的好处本质上和其他线程api一样, 方便

在 Android 平台上,协程有两个主要使用场景:

  • 1、线程切换,保证线程安全。
  • 2、处理耗时任务(比如网络请求、解析JSON数据、从数据库中进行读写操作等)。

Kotlin协程的原理

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

val call: Call

很明显我们需要处理很多的回调分支,如果业务多则更容易陷入「回调地狱」繁琐凌乱的代码中。

使用协程,同样可以像 Rx 那样有效地消除回调地狱,不过无论是设计理念,还是代码风格,两者是有很大区别的,协程在写法上和普通的顺序代码类似,同步的方式去编写异步执行的代码。使用协程改造后代码如下:

GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
    val result = userApi.getUserSuspend("suming")//网络请求(IO 线程)
    tv_name.text = result?.name //更新 UI(主线程)
}

这就是kotlin最有名的【非阻塞式挂起】,使用同步的方式完成异步任务,而且很简洁,这是Kotlin协程的魅力所在。之所有可以用看起来同步的方式写异步代码,关键在于请求函数getUserSuspend()是一个 挂起函数 ,被suspend关键字修饰,下面会介绍。

ui

在上面的协程的原理图解中,耗时阻塞的操作并没有减少,只是交给了其他线程。userApi.getUserSuspend("suming")真正执行的时候会切换到IO线程中执行,获取结果后最后恢复到主线程上,然后继续执行剩下的流程。

将业务流程原理拆分得更细致一点,在主线程中创建协程A中执行整个业务流程,如果遇到异步调用任务则协程A被挂起,切换到IO线程中创建子协程B,获取结果后再恢复到主线程的协程A上,然后继续执行剩下的流程。

ui

协程Coroutine虽然不能脱离线程而运行,但可以在不同的线程之间切换,而且一个线程上可以一个或多个协程。下图动态显示了进程 - 线程 - 协程微妙关系。

ui

此动图来源

三、基础

GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
   val result = userApi.getUserSuspend("suming")//网络请求(IO 线程)
   tv_name.text = result?.name //更新 UI(主线程)
}

上面就是启动协程的代码,启动协程的代码可以分为三部分:GlobalScopelaunchDispatchers,它们分别对应:协程的作用域、构建器和调度器。

1.协程的构建

上面的GlobalScope.launch()属于协程构建器Coroutine builders,Kotlin 中还有其他几种 Builders, 负责创建协程

  • runBlocking:T:顶层函数,创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T,一般在项目中不会使用,主要是为main函数和测试设计的。
  • launch: 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job对象。这是最常用的用于启动协程的方式。
  • async: 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。并返回Deffer对象,可通过调用Deffer.await()方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。

runBlocking

fun  runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
  • context:  协程的上下文,表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID等,默认值是当前线程上的事件循环。(这里的context和Android的context不同,后面会讲解到)
  • block:   协程执行体,是一个用suspend关键字修饰的一个无参,无返回值的函数类型。是一个带接收者的函数字面量,接收者是 CoroutineScope ,因此执行体包含了一个隐式的 CoroutineScope,所以在 runBlocking 内部可以来直接启动协程。
  • T:     返回值是泛型T,协程体block中最后一行返回的是什么类型T就是什么类型。

它是一个顶层函数,不是GlobalScope的 API,可以在任意地方独立使用。它能创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,它的目的是将常规的阻塞代码与以挂起suspend风格编写的库连接起来,常用于main函数和测试中。一般我们在项目中是不会使用的。

fun runBloTest() {
    print("start")
    //context上下文使用默认值,阻塞当前线程,直到代码块中的逻辑完成
    runBlocking {
        //这里是协程体
        delay(1000)//挂起函数,延迟1000毫秒
        print("runBlocking")
    }
    print("end")
}

打印数据如下:

ui

runBlocking.gif

只有在runBlocking协程体逻辑全部运行结束后,声明在runBlocking之后的代码才能执行,即runBlocking会阻塞其所在线程。

注意:runBlocking 虽然会阻塞当前线程的,但其内部运行的协程又是非阻塞的。

launch

launch是最常用的用于启动协程的方式,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即Job对象。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
  • context: 协程的上下文,表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID等,默认值是当前线程上的事件循环。
  • start:  协程启动模式,这些启动模式的设计主要是为了应对某些特殊的场景。业务开发实践中通常使用DEFAULTLAZY这两个启动模式就够了。
  • block:  协程代码,它将在提供的范围的上下文中被调用。它是一个用suspend(挂起函数)关键字修饰的一个无参,无返回值的函数类型。接收者是CoroutineScope的函数字面量。
  • Job:   协程构建函数的返回值,可以把Job看成协程对象本身,封装了协程中需要执行的代码逻辑,是协程的唯一标识,Job可以取消,并且负责管理协程的生命周期。

协程需要运行在协程上下文环境中 (即协程作用域,下面会讲解到),在非协程环境中launch有两种方式创建协程:

GlobalScope.launch()

在应用范围内启动一个新协程,不会阻塞调用线程,协程的生命周期与应用程序一致。表示一个不绑定任何Job的全局作用域,用于启动顶层协程,这些协程在整个应用程序生命周期中运行,不会提前取消(不存在Job)。

fun launchTest() {
    print("start")
    //创建一个全局作用域协程,不会阻塞当前线程,生命周期与应用程序一致
    GlobalScope.launch {
        //在这1000毫秒内该协程所处的线程不会阻塞
        //协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
        delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
        print("GlobalScope.launch")
    }
    print("end")//主线程继续,而协程被延迟
}

GlobalScope.launch()协程将线程的执行权交出去,该线程继续干它要干的事情,主线程继续,而协程被延迟,到时间后会恢复至此继续向下执行。

打印数据如下:

ui

launch1.gif

由于这样启动的协程存在组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,尤其是在 Android 客户端这种需要频繁创建销毁组件的场景,因此不推荐这种用法。

注意:这里说的是GlobalScope没有Job, 但是启动的launch是有Job的。 GlobalScope本身就是一个作用域, launch属于其子作用域。

CoroutineScope.launch()

启动一个新的协程而不阻塞当前线程,并返回对协程的引用作为一个Job。通过CoroutineContext至少一个协程上下文参数创建一个 CoroutineScope对象。协程上下文控制协程生命周期和线程调度,使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁,从而实现安全可靠地协程调用。这是在应用中最推荐使用的协程使用方式。

fun launchTest2() {
    print("start")
    //开启一个IO模式的协程,通过协程上下文创建一个CoroutineScope对象,需要一个类型为CoroutineContext的参数
    val job = CoroutineScope(Dispatchers.IO).launch {
        delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
        print("CoroutineScope.launch")
    }
    print("end")//主线程继续,而协程被延迟
}
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分