阿里P8大佬带你真正理解一波Kotlin协程

简介: 前言Kotlin协程,现在已经成为了面试甚至是工作中一个非常火的东西。本人在刚开始了解Kotlin协程的时候,断断续续看了网上不少文章,用长篇大论把Kotlin协程描述的非常玄乎,但是看完后还是依然云里雾里,所以决定来写一篇关于协程的文章,希望能够帮助大家能够更快的上手Kotlin协程.

1概述

目录



前言


Kotlin协程,现在已经成为了面试甚至是工作中一个非常火的东西。


本人在刚开始了解Kotlin协程的时候,断断续续看了网上不少文章,用长篇大论把Kotlin协程描述的非常玄乎,但是看完后还是依然云里雾里,所以决定来写一篇关于协程的文章,希望能够帮助大家能够更快的上手Kotlin协程.

注意:如果没有特殊提及,文中所有“协程”均代表“Kotlin协程”

2为什么要学习Kotlin协程?(官方版)

现在Android技术栈上的新东西层出不穷,kotlin、jetpack、flutter等等。很多人是为了准备面试而学习,所以往往往更偏向于去看一些概念性的东西,以便面试的时候能够蒙混过关。

但是我觉得,我们还是先要了解这个新的技术能够给我们的开发带来哪些实质性的帮助,我们再去针对性学习可能会更加有意义.


我们先来看看Kotlin官网是怎么体现使用协程的优势的


https://www.kotlincn.net/docs/reference/coroutines/basics.html


网上很多文章也用这个例子,也用这个官方例子来说明使用协程的优势,然后就说协程是什么轻量级的线程,又是什么用户态的,协程像线程但又不是线程...诸如此类。


所以很多人自认为学会了协程,最后就可能只能说出来使用协程的目的是比线程性能更好。


先不说这些概念对不对,我相信对于一个普通的Android开发来说,听到这些概念,第一反应肯定觉得协程这个东西非常的神秘且不好理解。


所以,不好理解我们就先不理解,我们先基于我们已有的知识来分析一下官网这个例子。

官网这个例子就是通过repeat函数启动了10000个协程,然后它让我们试一试使用Thread来实现会发生什么,也就是像下面这样:

repeat(100_000){    thread{       Thread.sleep(1000L)       print(""."")    }}

这个例子我们不用跑也知道大概会发生什么了。

但是,我想说的是,kotlin官方用这个例子真的有点不厚道了,用java底层的Thread类,和他们造出来的一个基于Thread类封装的“工具包”进行对比。

真正要比的话,我们用java的Executor和他比比?

repeat(100_000) {    val executor = Executors.newSingleThreadScheduledExecutor()    val task = Runnable {        print(".")    }    repeat(100_00) {        executor.schedule(task, 1, TimeUnit.SECONDS)    }}

<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;">我用上面这段程序跑了一下,用协程相对于java的线程池,并没有发现什么实质上的性能优势。感兴趣的也可以自己借助Android Studio Profiler试一试</pre>


所以,到目前为止,我们可以下一个结论。

使用Kotlin协程,本质上其实并没有比我们原先的开发模式有多大性能上的优势,因为我们所使用的的OkHttp、AsyncTask等内部都帮我们封装了线程池,而不是直接使用Thread类。

我上面还提到了一个可能有点争议的观点,Kotlin协程只是一个基于Thread类封装的”工具包“而已。但目前还没有得到证明,我们不妨继续往下看。

3Kotlin协程运行在单线程里面吗?

这里我截了一个百度搜索Kotlin协程,比较高赞的一篇文章,文章中截取了各种概念,到网上找了各种和协程相关的图,但我想说,这其实是误导人!

很简单,我们做一个小实验就能知道结果:

fun main(){    //在没有开启协程前,先打印一下进程名称和进程id    println(        "Main: " +                "threadName = " + Thread.currentThread().name                + " threadId = " + Thread.currentThread().id    )    //循环20次    repeat(20) {        GlobalScope.launch {            //开启协程后,先打印一下进程名称和进程id            println(                "IO: " +                        "threadName = " + Thread.currentThread().name                        + " threadId = " + Thread.currentThread().id            )            delay(1000L)        }    }}

日志打印结果:




发现了什么?所谓的协程完全就是开启了一个新的线程来执行任务,有些任务的线程名称和线程id还是完全一致的!这像不像java中的线程池?


看到这里,是不是有点颠覆你原来对协程的认识?难道网上的所有文章都是错误的?


其实网上大部分文章说的协程,指的可能都是其他语言的协程的特点,比如Go、Lua...


而我们要学的,是Kotlin协程,它不是真正意义上的协程,它也没有那么的神秘,本质上还是一套基于原生Java Thread API 的封装。只要你没有魔改JVM,start了几个线程,操作系统就会创建几个线程,Kotlin协程只是做了一个类似线程池的封装,根本谈不上什么性能更好。


总结下来Kotlin协程其实就是为了让我们更方便的来进行多线程开发而已,所以我们就抱着当初学习Handler、AsyncTask这些的心态来学习协程这个工具包就好了,不要想那么多复杂的东西来扰乱自己的思路。


4Kotlin协程有那么好用吗

一个新的技术的出现,大家往往从学习到真正在项目中实践往往需要一个过程,这里面有非常多的因素,有个人学习成本的因素,有公司方面的因素等等,但是最重要的,其实还是这个新的技术,到底是不是真的有取代我们现有技术的必要。接下来我就带大家一起来用用协程吧。


okhttp异步请求


这是我们常规的一个异步请求,通过回调的方式来处理请求结果


fun enqueue() {

使用协程的okhttp同步请求


这里先记住一句话,我们什么时候要用到协程的?


需要切换线程的时候要用到协程


所以想切到什么线程,就用GlobalScope.launch(Dispatchers.XX)切一下就好了,代码如下


fun execute() {    //切到io线程来执行同步请求    GlobalScope.launch(Dispatchers.IO) {        val result = ApiService.execute("/test")        //切到主线程来执行UI操作        GlobalScope.launch(Dispatchers.Main) {            tv_content.text = result        }    }}

看到这里,有的人可能觉得有点怪怪的,这看起来完全不足以吸引我使用协程,用回调不好吗?


kotlin毕竟是一门比较新的语言,所以在协程中,它同时给我们提供了一些非常实用的函数,所以上面的代码可以写成下面这样:


fun execute() {    GlobalScope.launch(Dispatchers.Main) {        //切到子线程执行任务        var result = withContext(Dispatchers.IO) {            ApiService.execute("/test")        }        //任务执行完后自动回到主线程        tv_content.text = result    }}

这个withContext函数的意义呢,就是能把耗时任务切到子线程去,然后任务执行完之后,又会自动切回主线程。


我们还可以继续优化一下代码,把数据请求和处理抽取到一个方法中,方便调用。


有人可能看到这里多了一个suspend关键字,这个其实就是告诉编译器这里要执行一个异步代码,调用者需要把我切到协程里,用什么切?


就是这个GlobalScope.launch(Dispatchers.Main) 。


不要想太多,就是一个编译检查而已,如果你不使用 GlobalScope.launch来调用suspend修饰的方法就不能编译通过


fun execute() {

到了这里,我相信很多喜欢回调的朋友们心中依然觉得,这依然不足以让我来使用协程。


我们继续往下看。


利用接口回调处理有上下文关联的任务


这时候有一个需求,我们需要先获取用户的token,再通过token查询用户名称


fun enqueue() {    //先获取token    ApiService.enqueue("/login", object : Callback {        override fun onResponse(call: Call, response: Response) {            val token = ...             //通过token请求用户信息            ApiService.enqueue("/getUserInfo", token, object : Callback {                override fun onResponse(call: Call, response: Response) {                    val user = ...                    runOnUiThread {                        //切换到主线程更新信息                        tv_content.text = user.name                    }                }                override fun onFailure(call: Call, e: IOException) {                }            })        }        override fun onFailure(call: Call, e: IOException) {        }    })}

使用协程处理有上下文关联的任务


fun execute() {    GlobalScope.launch(Dispatchers.Main) {        val token = login()        val user = getUsrInfo(token)        tv_content.text = user.name    }}

这时候利用协程的优势就明显了很多。


但是,就是有人深深的爱着回调,而且我知道你心里想的是什么,反正用这种多层回调的场景也不多,应用程序能跑起来不影响性能就好了。


这么说确实也没错,那么我们继续往下看。


前面这种情况,getUserInfo接口是依赖于login接口返回的token的,所以不可避免的使用了回调。


但是现在有一个场景,我们需要将接口A中的接口B中的数据进行合并展示,但是这两个接口在服务端的接口设计上是没有非常强的关联的,这时候出现了两种人。


第一种:想了想觉得没啥办法,然后还是按照先调用接口A,成功后再调用接口B,然后在接口B的回调中进行数据合并


第二种:觉得第一种方式不合理,所以去找服务端“撕逼”,告诉服务端把这两个接口的数据合并到一个接口中返回,客户端处理不了就找服务端呗。这番“撕逼”下来,你有可能成功了,也有可能失败了,但最终的结果都不是非常好。


我们想象一下,假设接口A耗时100ms,接口B耗时120ms,那么实际上在并发处理的情况下,你最快只需要120ms就可以将两个数据进行合并,但是使用回调的方式需要220ms(100ms+120ms)。


之前讲的都只是代码的美观层面的东西,到这里就是性能问题了,各种小的性能问题不解决,一个app怎么可能有比较好的用户体验呢。


我们来看看通过协程可以怎么做:


fun execute() {    GlobalScope.launch(Dispatchers.Main) {            //使用async发起两个异步请求        val one = async { one() }        val two = async { two() }                //使用await进行合并        val result = one.await() + two.await()        tv_content.text = result    }}

看到这里,我们再来看协程这个名字,英文名是Coroutine,中文全称叫做“协同程序”,结合我们前面说的内容,你是否对协程有了新的认识呢?


协程就是协同多个程序之间进行合作,帮助我们轻松的写出复杂的并发代码,甚至还能用非常简单的方式实现原本不可能实现的并发任务。这就是我们为什么要学习协程的理由。


5到底什么是非阻塞式挂起

网上很多文章都提到协程的挂起是非阻塞式的,挂起是什么呢?


就是我们前面说的withContext(Dispatchers.IO)挂起函数,当然还有delay、async,说白了只要不影响我们主线程的工作,那就是被挂起了,这里的”挂起“两个字用的非常玄乎,感觉像挂在我们的主线程,其实按照我上面的分析,更贴切的说法是”切到另一个线程“。


再来分析这个非阻塞式,阻塞很简单,就是字面意思,那既然是阻塞,那总得知道阻塞了什么吧?


在我们Android中,其实就是阻塞了主线程的运行,那反过来非阻塞式其实就是没有卡住主线程的运行.


所以,协程是非阻塞式挂起的是什么意思呢?就是一段程序切到另一个线程执行,不会卡住到主线程。


没错,这听起来就像是一句废话,但是网上很多文章却用一些更高级的词汇来说出来这个废话,让大家去花更多的心思去琢磨。不用想那么多,非阻塞式挂起其实就是这么简单~


6总结

本文只是给你讲了协程是什么,并用一个小例子给你展示了协程的作用,更多API使用方式还是推荐看官方文档哦.


最后做一个总结


协程(Coroutine)就是协同程序,而Kotlin协程就是一个基于Java Thread API封装的工具包,帮助我们轻松的写出复杂的并发代码

kotlin协程相较于线程池,并没有什么性能上的优势

非阻塞式挂起没什么特别的,java子线程也同样是非阻塞式的

如果你熟悉Rxjava,你再对比一下协程,你会发现协程比Rxjava还好用


相关文章
|
7月前
|
Java 数据库 Android开发
【专栏】Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
190 5
|
7月前
|
传感器 Android开发 开发者
构建高效Android应用:Kotlin的协程与Flow
【4月更文挑战第26天】随着移动应用开发的不断进步,开发者寻求更简洁高效的编码方式以应对复杂多变的业务需求。在众多技术方案中,Kotlin语言凭借其简洁性和强大的功能库逐渐成为Android开发的主流选择。特别是Kotlin的协程和Flow这两个特性,它们为处理异步任务和数据流提供了强大而灵活的工具。本文将深入探讨如何通过Kotlin协程和Flow来优化Android应用性能,实现更加流畅的用户体验,并展示在实际开发中的应用实例。
|
7月前
|
移动开发 Java Android开发
构建高效Android应用:Kotlin与协程的完美融合
【2月更文挑战第25天】 在移动开发领域,性能优化和应用响应性的提升是永恒的追求。随着Android Jetpack组件库的不断丰富,Kotlin语言已经成为Android开发的首选。而Kotlin协程作为一种新的并发处理方案,它以轻量级线程的形式,为开发者提供了简洁高效的异步编程手段。本文将深入探讨Kotlin协程在Android应用中的实践运用,以及如何通过这种技术改善用户界面的流畅度和后台任务的处理能力,进而构建出更高效、更稳定的Android应用。
|
7月前
|
安全 Android开发 开发者
构建高效Android应用:Kotlin与协程的完美结合
【2月更文挑战第30天】在移动开发领域,性能优化和流畅的用户体验是关键。本文深入探讨了如何通过结合Kotlin语言和协程技术来提升Android应用的性能和响应能力。我们将分析Kotlin的优势,介绍协程的基本概念,并通过实际案例展示如何在应用中实现协程以简化异步编程,从而提供更加高效的解决方案。
|
24天前
|
Java 编译器 测试技术
Kotlin31 协程如何与 Java 进行混编?
Kotlin31 协程如何与 Java 进行混编?
23 2
Kotlin31 协程如何与 Java 进行混编?
|
2月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
34 1
|
4月前
|
调度 开发者 UED
Kotlin 中的协程是什么?
【8月更文挑战第31天】
322 0
|
6月前
|
存储 Java 调度
Android面试题之Kotlin 协程的挂起、执行和恢复过程
了解Kotlin协程的挂起、执行和恢复机制。挂起时,状态和上下文(局部变量、调用栈、调度器等)被保存;挂起点通过`Continuation`对象处理,释放线程控制权。当恢复条件满足,调度器重新分配线程,调用`resumeWith`恢复执行。关注公众号“AntDream”获取更多并发知识。
137 2
|
7月前
|
移动开发 Android开发 开发者
构建高效Android应用:Kotlin与协程的完美融合
【5月更文挑战第25天】 在移动开发的世界中,性能和响应性是衡量应用质量的关键指标。随着Kotlin的流行和协程的引入,Android开发者现在有了更强大的工具来提升应用的性能和用户体验。本文深入探讨了Kotlin语言如何与协程相结合,为Android应用开发带来异步处理能力的同时,保持代码的简洁性和可读性。我们将通过实际案例分析,展示如何在Android项目中实现协程,以及它们如何帮助开发者更有效地管理后台任务和用户界面的流畅交互。
|
7月前
|
移动开发 监控 Android开发
构建高效安卓应用:Kotlin 协程的实践与优化
【5月更文挑战第16天】 在移动开发领域,性能优化一直是开发者们追求的重要目标。特别是对于安卓平台来说,由于设备多样性和系统资源的限制,如何提升应用的响应性和流畅度成为了一个关键议题。近年来,Kotlin 语言因其简洁、安全和高效的特点,在安卓开发中得到了广泛的应用。其中,Kotlin 协程作为一种轻量级的并发解决方案,为异步编程提供了强大支持,成为提升安卓应用性能的有效手段。本文将深入探讨 Kotlin 协程在安卓开发中的应用实践,以及通过合理设计和使用协程来优化应用性能的策略。
68 8