电子说
公司开启新项目了,想着准备亮一手 Kotlin 协程应用到项目中去,之前有对 Kotlin 协程的知识进行一定量的学习,以为自己理解协程了,结果……实在拿不出手!
为了更好的加深记忆和理解,更全面系统深入地学习 Kotlin 协程的知识,协程将分为三部分来讲解,本文是第一篇
协程的概念在1958年就开始出现(比线程还早), 目前很多语言开始原生支, Java 没有原生协程但是大型公司都自己或者使用第三方库来支持协程编程, 但是Kotlin原生支持协程。
Android 中的每个应用都会运行一个主线程,它主要是用来处理 UI,如果主线程上需要处理的任务太多,应用就感觉被卡主一样影响用户体验,得让那些耗时的任务不阻塞主线程的运行。要做到处理网络请求不会阻塞主线程,一个常用的做法就是使用回调,另一种是使用协程。
很多人都会问协程是什么?这里引用官方的解释:
1.协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
2.协程是一种并发设计模式。
协程就像轻量级的线程,为什么是轻量的?因为协程是依赖于线程,一个线程中可以创建N个协程, 很重要的一点就是协程挂起时不会阻塞线程 ,几乎是无代价的。而且它 基于线程池API ,所以在处理并发任务这件事上它真的游刃有余。
协程只是一种概念,它提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法: 协程挂起和恢复 。 本质上Kotlin协程就是作为在Kotlin语言上进行异步编程的解决方案,处理异步代码的方法 。
有可能有的同学问了,既然它基于线程池,那我直接使用线程池或者使用 Android 中其他的异步任务解决方案,比如 Handler、AsyncTask、RxJava等,不更好吗?
协程可以 使用阻塞的方式写出非阻塞式的代码 ,解决并发中常见的回调地狱。消除了并发任务之间的协作的难度,协程可以让我们轻松地写出复杂的并发代码。一些本来不可能实现的并发任务变的可能,甚至简单,这些才是协程的优势所在。
kotlin的协程实现分为了两个层次:
在 project
的 gradle
添加 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 平台上,协程有两个主要使用场景:
JSON
数据、从数据库中进行读写操作等)。我们使用 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
关键字修饰,下面会介绍。
在上面的协程的原理图解中,耗时阻塞的操作并没有减少,只是交给了其他线程。userApi.getUserSuspend("suming")
真正执行的时候会切换到IO线程中执行,获取结果后最后恢复到主线程上,然后继续执行剩下的流程。
将业务流程原理拆分得更细致一点,在主线程中创建协程A
中执行整个业务流程,如果遇到异步调用任务则协程A
被挂起,切换到IO线程中创建子协程B
,获取结果后再恢复到主线程的协程A
上,然后继续执行剩下的流程。
协程Coroutine虽然不能脱离线程而运行,但可以在不同的线程之间切换,而且一个线程上可以一个或多个协程。下图动态显示了进程 - 线程 - 协程微妙关系。
此动图来源
GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
val result = userApi.getUserSuspend("suming")//网络请求(IO 线程)
tv_name.text = result?.name //更新 UI(主线程)
}
上面就是启动协程的代码,启动协程的代码可以分为三部分:GlobalScope
、launch
、Dispatchers
,它们分别对应:协程的作用域、构建器和调度器。
上面的GlobalScope.launch()
属于协程构建器Coroutine builders
,Kotlin 中还有其他几种 Builders, 负责创建协程 :
runBlocking:T
:顶层函数,创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T
,一般在项目中不会使用,主要是为main函数和测试设计的。launch
: 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job
对象。这是最常用的用于启动协程的方式。async
: 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。并返回Deffer
对象,可通过调用Deffer.await()
方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
context
和Android的context
不同,后面会讲解到)CoroutineScope
,所以在 runBlocking
内部可以来直接启动协程。T
,协程体block
中最后一行返回的是什么类型T
就是什么类型。它是一个顶层函数,不是GlobalScope
的 API,可以在任意地方独立使用。它能创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,它的目的是将常规的阻塞代码与以挂起suspend
风格编写的库连接起来,常用于main
函数和测试中。一般我们在项目中是不会使用的。
fun runBloTest() {
print("start")
//context上下文使用默认值,阻塞当前线程,直到代码块中的逻辑完成
runBlocking {
//这里是协程体
delay(1000)//挂起函数,延迟1000毫秒
print("runBlocking")
}
print("end")
}
打印数据如下:
runBlocking.gif
只有在runBlocking
协程体逻辑全部运行结束后,声明在runBlocking
之后的代码才能执行,即runBlocking
会阻塞其所在线程。
注意:runBlocking
虽然会阻塞当前线程的,但其内部运行的协程又是非阻塞的。
launch
是最常用的用于启动协程的方式,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即Job
对象。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
suspend
(挂起函数)关键字修饰的一个无参,无返回值的函数类型。接收者是CoroutineScope
的函数字面量。Job
看成协程对象本身,封装了协程中需要执行的代码逻辑,是协程的唯一标识,Job可以取消,并且负责管理协程的生命周期。协程需要运行在协程上下文环境中 (即协程作用域,下面会讲解到),在非协程环境中launch
有两种方式创建协程:
在应用范围内启动一个新协程,不会阻塞调用线程,协程的生命周期与应用程序一致。表示一个不绑定任何Job
的全局作用域,用于启动顶层协程,这些协程在整个应用程序生命周期中运行,不会提前取消(不存在Job
)。
fun launchTest() {
print("start")
//创建一个全局作用域协程,不会阻塞当前线程,生命周期与应用程序一致
GlobalScope.launch {
//在这1000毫秒内该协程所处的线程不会阻塞
//协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
print("GlobalScope.launch")
}
print("end")//主线程继续,而协程被延迟
}
GlobalScope.launch()
协程将线程的执行权交出去,该线程继续干它要干的事情,主线程继续,而协程被延迟,到时间后会恢复至此继续向下执行。
打印数据如下:
launch1.gif
由于这样启动的协程存在组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,尤其是在 Android 客户端这种需要频繁创建销毁组件的场景,因此不推荐这种用法。
注意:这里说的是GlobalScope
没有Job
, 但是启动的launch
是有Job
的。 GlobalScope
本身就是一个作用域, 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")//主线程继续,而协程被延迟
}
全部0条评论
快来发表一下你的评论吧 !