Kotlin协程入门到体验

Kotlin协程那么神奇,那具体该如何使用呢?

1.添加依赖
Kotlin协程不属于Kotlin语言本身,使用之前必须手动引入。在Android平台上使用可以添加Gradle依赖:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

2.启动协程
首先看下如下代码:

GlobalScope.launch {
    delay(1000L)    
    println("Hello,World!")
}

上述代码使用launch方法启动了一个协程,launch后面的花括号就是协程,花括号内的代码就是运行在协程内的代码。

接着来深入了解一下launch方法的声明:

public fun CoroutineScope.launch(    
    context: CoroutineContext = EmptyCoroutineContext,    
    start: CoroutineStart = CoroutineStart.DEFAULT,    
    block: suspend CoroutineScope.() -> Unit): Job {...}

可以看到launch方法是CoroutineScope的拓展方法,也就是说我们启动协程要在一个指定的CoroutineScope上来启动。CoroutineScope翻译过来就是“协程范围”,指的是协程内的代码运行的时间周期范围,如果超出了指定的协程范围,协程会被取消执行,上面第一段代码中的GlobalScope指的是与应用进程相同的协程范围,也就是在进程没有结束之前协程内的代码都可以运行。除此之外为了方便我们的使用,在Google的Jetpack中也提供了一些生命周期感知型协程范围。实际开发中我们可以方便地选择适当的协程范围来为耗时操作(网络请求等)指定自动取消执行的时机,详情见:https://developer.android.google.cn/topic/libraries/architecture/coroutines

接着可以看下launch方法的其他参数:

context:协程上下文,可以指定协程运行的线程。默认与指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默认运行在一个后台工作线程内。也可以通过显示指定参数来更改协程运行的线程,Dispatchers提供了几个值可以指定:Dispatchers.Default、Dispatchers.Main、Dispatchers.IO、Dispatchers.Unconfined。
start:协程的启动模式。默认的(也是最常用的)CoroutineStart.DEFAULT是指协程立即执行,除此之外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED。
block:协程主体。也就是要在协程内部运行的代码,可以通过lamda表达式的方式方便的编写协程内运行的代码。
CoroutineExceptionHandler:除此之外还可以指定CoroutineExceptionHandler来处理协程内部的异常。
返回值Job:对当前创建的协程的引用。可以通过Job的start、cancel、join等方法来控制协程的启动和取消。

启动协程不是只有launch一个方法的,还有async等其他方法可以启动协程,不过launch是最常用的一种方法,其他的方法大家可以去自行了解。

3.调用挂起函数
回到上面的代码:

println("Start")
GlobalScope.launch(Dispatchers.Main) {
    delay(1000L)
    println("Hello World")
}
println("End")

首先通过GlobalScope.launch启动了一个协程,这里指定协程运行的线程为主线程,接着协程内只有两行代码,协程启动之后就立即执行。首先直接输出了"Start"和"End",接着1秒钟后又输出了"Hello World"。这结果看起来看似顺理成章,因为我们使用非常相似的Thread相关的代码也完全可以实现以上代码的效果:

println("Start")
Thread {
    Thread.sleep(1000L)
    println("Hello World")
}.start()
println("End")

两段代码看起来长得几乎一模一样,运行结果也完全一致。那究竟协程的神奇之处在哪里呢?顺序编写异步代码有体现在什么地方呢?

我们在上面两段代码的所有输出的位置上全部加上输出当前线程名的操作:

//协程代码
println("Start ${Thread.currentThread().name}")
GlobalScope.launch(Dispatchers.Main) {
    delay(1000L)
    println("Hello World ${Thread.currentThread().name}")
}
println("End ${Thread.currentThread().name}")
//线程代码
println("Start ${Thread.currentThread().name}")
Thread {
    Thread.sleep(1000L)
    println("Hello World ${Thread.currentThread().name}")
}.start()
println("End ${Thread.currentThread().name}")

线程代码输出为:“Start main”->“End main”->“Hello World Thread-2”。这个结果也很好理解,首先在主线程里输出"Start",接着创建了一个新的线程并启动后阻塞一秒,这时主线程继续向下执行输出"End",这时启动的线程阻塞时间结束,在当前创建的线程输出"Hello World"。

协程代码输出为:“Start main”->“End main”->“Hello World main”。前两个输出很好理解与上面一致,但是等待一秒之后协程里面的输出结果却显示当前输出的线程为主线程!这是个很神奇的事情,输出"Start"之后就立即输出了"End"说明了我们的主线程并没有被阻塞,等待的那一秒钟被阻塞的一定是其他线程。但是阻塞结束后的输出却发生在主线程中,这说明了一件事:协程中的代码自动地切换到其他线程之后又自动地切换回了主线程!这不正是我们一直想要的效果吗?

还记得上一篇文章中说到的吗?这个例子中delay和println两行代码紧密地写在协程之中,他们的执行也严格按照从上到下一行一行地顺序执行,但是这两行的代码却运行在完全不同的两个线程中,这就是我们想要的“既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行”的“顺序编写异步代码的效果”。顺序编写保证了逻辑上的直观性,协程的自动线程切换又保证了代码的非阻塞性。

那为什么协程中的delay函数没有在主线程中执行呢?而且执行完毕为什么还会自动地切回主线程呢?这是怎么做到的呢?我们可以来看一下delay函数的定义:

public suspend fun delay(timeMillis: Long) {...}

可以发现这个函数与正常的函数相比前面多了一个suspend关键字,这个关键字翻译过来就是“挂起”的意思,suspend关键字修饰的函数也就叫“挂起函数”。关于挂起函数有个规定:挂起函数必须在协程或者其他挂起函数中被调用,换句话说就是挂起函数必须直接或者间接地在协程中执行。

关于挂起的概念大家不要理解错了,挂起的不是线程而是协程。遇到了挂起函数,协程所在的线程不会挂起也不会阻塞,但是协程被挂起了,就是说协程被挂起时当前协程与它所运行在的线程脱钩了。线程继续执行其他代码去了,而协程被挂起等待着,等待着将来线程继续回来执行自己的代码。也就是协程中的代码对线程来说是非阻塞的,但是对协程自己本身来说是阻塞的。换句话说,协程的挂起阻塞的不是线程而是协程。

所以说,协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的重新代码进入协程所在线程的过程。协程就是通过的这个挂起恢复机制进行线程的切换。

4.线程切换
既然协程执行到了挂起函数会被挂起,那么是suspend关键字进行的线程切换吗?怎么指定切换到哪个线程呢?对此我们可以做一个简单的试验:

GlobalScope.launch(Dispatchers.Main) {
    println("Hello ${Thread.currentThread().name}")    
    test()
    println("End ${Thread.currentThread().name}")
}

suspend fun test(){
    println("World ${Thread.currentThread().name}")
}

执行结果为:Hello main -> World main -> End main,也就是说这个suspend函数仍然运行在主线程中,suspend并没有切换线程的作用。

实际上我们可以withContext方法来在suspend函数中进行线程的切换:

GlobalScope.launch(Dispatchers.Main) {
    println("Hello ${Thread.currentThread().name}")    
    test()
    println("End ${Thread.currentThread().name}")
}

suspend fun test(){
   withContext(Dispatchers.IO){
        println("World ${Thread.currentThread().name}")
   }
}

执行的结果为:Hello main -> World DefaultDispatcher-worker-1 -> End main,这说明我们的suspend函数的确运行在不同的线程之中了。就是说实际是上withContext方法进行的线程切换的工作,那么suspend关键字有什么用处呢?

其实,忽略原理只从使用上来讲,suspend关键字只起到了标志这个函数是一个耗时操作,必须放在协程中执行的作用。关于线程切换其实还有其他方法,但是withContext是最常用的一个,其他的如感兴趣可以自行了解。

5.顺序执行与并发执行
5.1 顺序执行
这是上一篇文章中演示回调地狱的代码:

//客户端顺序进行三次网络异步请求,并用最终结果更新UI

request1(parameter) { value1 ->
    request2(value1) { value2 ->
        request3(value2) { value3 ->
            updateUI(value3)            
        } 
    }              
}

我们试着用刚刚学到的协程的方式来改进这个代码:

//用协程改造回调代码

GlobalScope.launch(Dispatchers.Main) {
    //三次请求顺序执行
    val value1 = request1(parameter)
    val value2 = request2(value1)
    val value3 = request2(value2)
    //用最终结果更新UI
    updateUI(value3)
}

//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}

前提是request相关的API已经改造成了适应协程的方式,并在内部进行了线程切换。这样代码看起来是不是整洁多了?没有了烦人的嵌套,所有的逻辑都体现在了代码的先后顺序上了,是不是一目了然呢?

5.2 并发执行
那么接下来实现一些有挑战性的东西:如果三次网络请求并不存在前后的依赖关系,也就是说三次请求要并发进行,但是最终更新UI要将三次请求的结果汇总才可以。这样的需求如果没有RxJava或Kotlin协程这种强大的工具支持,单靠自己编码实现的确是一个痛苦的过程。

不过Kotlin协程提供了一种简单的方案:async await方法。

//并发请求
GlobalScope.launch(Dispatchers.Main) {
    //三次请求并发进行
    val value1 = async { request1(parameter1) }
    val value2 = async { request2(parameter2) }
    val value3 = async { request3(parameter3) }
    //所有结果全部返回后更新UI
    updateUI(value1.await(), value2.await(), value3.await())
}

//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}

上面的代码中我们用async方法包裹执行了suspend方法,接着在用到结果的时候使用了await方法来获取请求结果,这样三次请求就是并发进行的,而且三次请求的结果都返回之后就会切回主线程来更新UI。

5.3 复杂业务逻辑
实际开发遇到了串行与并行混合的复杂业务逻辑,那么我们当然也可以混合使用上面介绍的方法来编写对应的代码。比如这样的业务逻辑:request2和request3都依赖于request1的请求结果才能进行,request2和request3要并发进行,更新UI依赖request2和request3的请求结果。

这样的复杂业务逻辑,如果自己实现是不是感觉要被逼疯?来看看Kotlin协程给出的方案:

//复杂业务逻辑的Kotlin协程实现
GlobalScope.launch(Dispatchers.Main) {
    //首先拿到request1的请求结果
    val value1 = request1(parameter1)
    //将request1的请求结果用于request2和request3两个请求的并发进行
    val value2 = async { request2(value1) }
    val value3 = async { request2(value1) }
    //用request2和request3两个请求结果更新UI
    updateUI(value2.await(), value3.await())
}

//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}

怎么样?发现没有,无论怎样的复杂业务逻辑,用Kotlin协程表达出来始终是从上到下整齐排列的四行代码,无任何耦合嵌套,有没有从中感受到Kotlin协程的这股化腐朽为神奇的神秘力量。

了解了Kotlin协程的用法之后,是不是迫不及待地想要在实际Android项目中使用它了?接下来我们来在项目中使用Kotlin协程的最佳实践。
————————————————
版权声明:本文为CSDN博主「代码都tm飞了」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/NJP_NJP/article/details/103513719

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容

  • 1. 前言 随着 Kotlin 的不断更新以及官方的推荐加持,越来越多的项目开始接受 Kotlin 作为主要的编写...
    nanchen2251阅读 2,166评论 2 14
  • 在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种...
    Android开发指南阅读 779评论 0 2
  • 在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种...
    业志陈阅读 1,001评论 0 5
  • 本文主要介绍协程长什么样子, 协程是什么东西, 协程挂起的实现原理以及整理了协程学习的资料. 协程 HelloWo...
    JBD阅读 1,043评论 0 1
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 120,787评论 2 7