由于协程在概念上关于Java开发者来说便是个新东西,所以关于大多数人来说,别说怎样用了,我连它是什么东西都没看了解。

协程是是什么

协程并不是Kotlin提出来的新概念,其他的一些编程言语,例如:Go、Python等都能够在言语层面上完成协程,乃至是Java,也能够经过运用扩展库来间接地支撑协程。

当在网上查找协程时,咱们会看到:

  • Kotlin官方文档说「本质上,协程是轻量级的线程」。
  • 许多博客说到「不需求从用户态切换到内核态」、「是协作式的」等等。

作为Kootlin协程的初学者,这些概念并不是那么简单让人了解。这些往往是作者依据自己的经验总结出来的,只看成果,而不管进程就不简单了解协程

「协程Coroutines」源自Simula和Modula-2言语,这个术语早在1958年就被Melvin Edward Conway创造并用于构建汇编程序,说明协程是一种编程思维,并不局限于特定的言语。

Go言语也有协程,叫Goroutines,从英文拼写就知道它和Coroutines仍是有些不同的(规划思维上是有联系的),不然Kotlin的协程完全能够叫Koroutines了。

因此,对一个新术语,咱们需求知道什么是「规范」术语,什么是变种。

当咱们讨论协程和线程的联系时,很简单堕入中文的误区,两者都有一个「程」字,就觉得有联系,其实就英文而言,Coroutines和Threads便是两个概念。

从Android开发者的视点去了解它们的联系:

  • 咱们一切的代码都是跑在线程中的,而线程是跑在进程中的。
  • 协程没有直接和操作系统办理,但它不是空中楼阁,它也是跑在线程这种的,能够试试单线程,也能够是多线程。
  • 单线程中的协程总的履行时间并不会比不用协程少。
  • Android系统上,假如在主线程进行网络恳求,会抛出NetworkOnMainThreadException关于在主线程上的协程也不破例,这种场景运用协程仍是要切线程的。

协程规划的初衷是为了处理并发问题,让「协作式多使命」完成起来愈加便利。这儿就先不展开『协作式多使命』的概念,等咱们学会了怎样用再讲。

协程便是Kotlin供给的一套线程封装的API,但并不是说协程便是为线程而生的。

不过,咱们学习Kotlin中的协程,一开端的确能够从线程操控的视点来切入。由于在Kotlin中,协程的一个典型的运用场景便是线程操控。就像Java中的Executor和Android的AsyncTask,Kotlin中的协程也有对Thread API的封装,让咱们能够在写代码时,不用重视多线程就能够很便利地写出并发操作。

在Java中要完成并发操作一般需求敞开一个Thread:

new Thread(new Runnable() {
    @Override
    public void run() {
        ...
    }
}).start();

这儿仅仅仅仅敞开了一个新线程,至于他何时完毕、履行成果怎样样,咱们在主线程中是无法知道的。

Kotlin中相同能够经过线程的办法去写:

Thread({
    ...
}).start()

能够看到,和Java相同也摆脱不了直接运用Thread的那些苦难和不便利。

  • 线程是什么时分履行完毕。
  • 线程间的相互通讯
  • 多个线程的办理

咱们能够用Java的Executor线程池来进行线程办理:

val executor = Executors.newCachedThreadPool()
executor.execute({
    ...
})

用Android的AsyncTask来处理线程间通讯:

object: AsyncTask<T0, T1, T2> {
    override fun doInBackground(vararg args: T0): String { ... }
    override fun onProgressUpdate(vararg args: T1) { ... }
    override fun onPostExecute(t3: T3) { ... }
}

AsyncTask是Android对线程池Executoor的封装,但它的缺陷也很明显:

  • 需求处理许多回调,假如事务多则简单堕入「回调阴间」。
  • 硬是把事务拆分成了前台、中间更新、后台三个函数。

看到这儿你很天然想到运用RxJava处理回调阴间,它的确能够很便利地处理上面的问题。

RxJava,准确来讲是ReactiveX在Java上的完成,是一种响应式程序框架,咱们经过它供给的「Observable」的编程范式进行链式调用,能够很好地消除回调。

运用协程,相同能够像Rx那样有效地消除回调阴间,不过无论是规划理念,仍是代码风格,两者是有很大差异的,协程在写法上和普通的次序代码类似。

这儿并不会比较RxJava和协程哪个好,或许讨论谁替代谁的问题,这儿只给出一个建议,最好都去了解下,由于协程和Rx的规划思维原本就不同。

下面的比方是运用协程进行网络恳求获取用户信息并显现到UI控件上:

launch({
    val user = api.getUser() // 网络恳求(IO 线程)
    nameTv.text = user.name // 更新UI(主线程)
})

这儿仅仅展现了一个代码片段,launch并不是一个顶层函数,它必须在一个目标中运用,这儿只关心它内部事务逻辑的写法。

launch函数加上完成在{}中详细的逻辑,就构成了一个协程。

咱们一般做网络恳求,要不就传一个callback,要不便是在IO线程里边进行堵塞式的同步调用,而在这段代码中,上下两个句子分别作业在两个线程里,但写法上看起来和普通的单线程代码相同。

这儿的api.getUser是一个挂起函数,所以能够保证nameTv.text的正确赋值,这就触及到了协程中最闻名的「非堵塞式挂起」。这个名词看起来不是那么简单了解,咱们后续的文章就会专门对这个概念进行解说。现在先把这个概念放下,只需求记住协程便是这样写的就行了。

这种「用同步的办法写异步的代码」看起来很便利吧,那么咱们来看看协程详细好在哪里。

协程好在哪

开端之前

先了解一下「闭包」这个概念,调用Kotlin协程中的API,经常会用到闭包写法。

其实闭包并不是Kotlin中的新概念,在Java 8中就已经支撑。

咱们先以Thread为例,来看看什么是闭包:

// 创立一个Thread的完好写法
Thread(object: Runnable {
    override fun run() {
        ...
    }
})
// 满足SAM,先简化为
Thread({
    ...
})
// 运用闭包,再简化为
Thread {
    ...
}

形如Thread { ... }这样的结构中{}便是一个闭包。

在Kotlin中有这样一个语法糖:当函数的最终一个参数是lambda表达式时,能够将lambda写在括号外。这便是它的必报准则。

在这儿需求一个类型为Runnable的参数,而Runnable是一个接口,且只界说了一个函数run,这种状况满足了Kotlin的SAM,能够转换成传递一个lambda表达式(第二段),由于是最终一个参数,依据闭包准则咱们就能够直接写成Thread {...}(第三段)的办法。

关于上文所运用的launch函数,能够经过闭包来进行简化:

launch {
    ...
}

基本运用

前面说到,launch函数不是顶层函数,是不能直接用的,能够运用下面三种办法来创立协程:

// 办法一,运用runBlocking顶层函数
runBlocking {
    getImage(imageId)
}
// 办法二,运用GlobalScope单例目标
//            能够直接调用launch敞开协程
GlobalScope.launch {
    getImage(imageId)
}
// 办法三,自行经过CoroutineContext创立一个CoroutineScope目标
//                         需求一个类型为CoroutineContext的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    getImage(imageId)
}
  • 办法一一般适用于单元测试的场景,而事务开发中不会用到这种办法,由于它是线程堵塞的。
  • 办法二和运用runBlocking的差异在于不会堵塞线程。但在Android开发中相同不引荐这种用法,由于它的生命周期会和app共同,且不能取消(什么是协程的取消后边的文章会讲)
  • 办法三是比较引荐的运用办法,咱们能够经过context参数去办理和操控协程的生命周期(这儿的context和Android里的不是一个东西,是一个更通用的概念,会有一个Android渠道的封装来合作运用)。

关于CoroutineScopeCoroutineContext的更多内后后边文章再讲。

协程最常用的功能是并发,而并发的典型场景便是多线程。能够运用Dispatcher.IO参数把使命切到IO线程履行:

coroutineScope.launch(Dispatchers.IO) {
    ...
}

也能够运用Dispatchers.Main参数切换到主线程:

coroutineScope.launch(Dispatchers.Main) {
    ...
}

所以在「协程是什么」一节中讲到的异步恳求的比方完好写出来是这样的:

coroutineScope.launch(Dispatchers.Main) {// 在主线程敞开协程
    val user = api.getUser() // IO线程履行网络恳求
    nameTv.text = user.name // 主线程更新UI
}

而经过Java完成以上逻辑,咱们一般需求这样写:

api.getUser(new Callback<User>() {
    @Override
    public void success(User user) {
        runOnUnThread(new Runnable() {
            @Override
            public void run() {
                nameTv.setText(user.name);
            }
        });
    }
    @Override
    public void failure(Exception e) {
        ...
    }
});

这种回调式的写法,打破了代码的次序结构和完好性,读起来相当难受。

协程的「1到0」

关于回调式的写法,假如并发场景再复杂一些,代码的嵌套或许会更多,这样的话,保护起来就十分麻烦。但假如你运用了Kotlin协程,多层网络恳求只需求这么写:

coroutineScope.launch(Dispatchers.Main) {// 开端协程:主线程
    val token = api.getToken() //网络恳求:IO线程
    val user = api.getUser(token) //网络恳求:IO线程
    nameTv.text = user.name // 更新UI:主线程
}

假如遇到的场景是多个网络恳求等待一切恳求完毕之后再对UI进行更新。比方以下两个恳求:

api.getAvatat(user, callback)
api.getCompanyLogo(user, callback)

假如运用回调的写法,那么代码或许写起来即困难又别扭。于是咱们或许会选择退让,经过先后恳求代替同时恳求:

api.getAvatar(user) { avatar ->
    api.getCompanyLogo(user) { logo -> 
        show(merge(avatar, logo))
    }
}

在实践开发中假如这样写,原本能够并行处理的恳求被强制经过串行的办法去完成,或许会导致等待时间长了一倍,也便是功能差了一倍。

而假如运用协程,能够直接把两个并行恳求写成上下两行,最终再把成果进行兼并即可:

coroutineScope.launch(Dispatchers.Main) {
    //        async函数之后再讲
    val avatar = async { api.getAvater(user) } // 获取用户头像
    val logo = async { api.getCompanyLogo(user) } //获取用户所在公司的logo
    val merged = suspendingMerge(avatar, logo) // 兼并成果
    show(merged)// 更新UI
}

能够看到,即使是比较复杂的并行网络恳求,也能够经过协程写出结构清晰的代码。需求注意的是suspendingMerge并不是协程API中供给的办法,而是咱们自界说一个可「挂起」的成果兼并办法。至于挂起详细是什么,能够看下一篇文章。

让复杂的并发代码,写起来变得简单且清晰,是协程的优势。

这儿,两个没有相关项的后台使命,由于用了协程,被组织的明了解白,相互之间合作得很好,也便是咱们之前说的「协作式使命」。

原本需求回调,现在直接没有回调,这种从1到0的规划思维真是妙哉。

在了解了协程的效果和优势之后,咱们再来看看协程试试怎样是运用的。

协程怎样用

在项目中配置对Kotlin协程的支撑

在运用协程之前,咱们需求在build.gradle文件中增加对Kotlin协程的依靠:

  • 项目根目录下的build.gradle:
buildscript {
    ...
    // 
    ext.kotlin_coroutines = '1.3.1'
    ...
}
  • Moudle下的build.gradle
dependencies {
    ...
    //依靠协程中心库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
    //依靠当时渠道所对应的渠道
}

Kotlin协程是以官方扩展库的办法进行支撑的。并且,咱们所运用的「中心库」和「渠道库」的版别应该保持共同。

  • 中心库中包括的代码主要是协程的公共API部分。有了这一层公共代码,才使得协程在各个渠道上的接口得到一致。
  • 渠道库中包括的代码主要是协程框架在详细渠道的详细完成办法。由于多线程在各个渠道的完成办法是有所差异的。

完成了以上的准备作业就能够开端运用协程了。

开端运用协程

协程最简单的运用办法,其实在前面章节已经看到了。咱们能够经过一个launch函数完成线程切换的功能:

coroutineScope.launch(Dispatchers.IO) {
    ...
}

这个launch函数,它详细的含义是:我要创立一个新的协程,并在指定的线程上运转它。这个被创立、被运转的所谓「协程」是谁?便是你传给launch的那些代码,这一段连续代码叫做一个「协程」。

所以,什么时分用协程?当你需求切线程或许指定线程的时分。你要在后台履行使命?切!

launch(Dispatchers.IO) {
    val image = getImage(imageId)
}

然后需求在前台更新界面?再切!

coroutineScope.launch(Dispatchers.IO) {
    val image = getImage(imageId)
    launch(Dispatchers.Main) {
        avatarIv.setImageBitmmap(image)
    }
}

如同有点不对劲?这不仍是有嵌套嘛。

假如仅仅运用launch函数,协程并不能比线程做更多的事。不过协程中确有一个很实用的函数:withContext。这个函数能够切换到指定的线程,并在闭包内的逻辑履行完毕之后,主动把线程切回来持续履行。那么能够将上面的代码写成这样:

coroutineScope.launch(Dispatchers.Main) {// 在UI线程开端
    val image = withContext(Dispatchers.IO) {//切换到IO线程,并在履行完成后切回UI线程
        getImage(image)// 将会运转在IO线程
    }
    avatarIv.setImageBitmap(image)// 回到UI线程更新UI
}

这种写法看上去如同和方才那种差异不大,但假如你需求频频地进行线程切换,这种写法的优势就会表现出来。能够参考下面的对比:

//第一种写法
coroutineScope.launch(Dispatchers.IO) {
    ...
    launch(Dispatchers.Main) {
        ...
        launch(Dispatchers.IO) {
            ...
            launch(Dispatchers.Main) {
                ...
            }
        }
    }
}
// 经过第二种写法来完成相同的逻辑
coroutineScope.launch(Dispatchers.Main) {
    ...
    withContext(Dispatchers.IO) {
        ...
    }
    ...
    withContext(Dispatchers.IO) {
        ...
    }
    ...
}

由于能够“主动切回来”,消除了并发代码在协作时的嵌套。由于消除了嵌套联系,咱们乃至能够把withContext放进一个单独的函数里边:

launch(Dispatchers.Main) {// 在UI线程开端
    val image = getImage(imageId)
    avatatIv.setImageBitmap(image)//履行完毕后,主动切换回UI线程
}
fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
    ...
}

这便是之前说的「用同步的办法写异步的代码」了

不过哦假如仅仅这样写,编译器是会报错的:

fun getImage(iamgeId: Int) = withContext(Dispatchers.IO) {
    // IDE报错,Suspend function 'withContext' should be called only from a coroutine or another suspend function
}

意思是说,withContext是一个suspend函数,它需求再协程或许是另一个suspend函数中调用。

suspend

suspend是Kotlin协程最中心的关键字,简直一切介绍Kotlin协程的文章和演讲都会说到它。它的中文意思是「暂停」或许「可挂起」。假如你去看一些技能博客或官方文档的时分,大概能够了解到:「代码履行到suspend函数的时分会「挂起」,并且这个「挂起」是非堵塞式的,它不会堵塞你当时的线程。」

上面报错的代码,其实只需求再前面一个suspend就能够编译经过:

suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
    ...
}

练习题

  1. 敞开一个协程,并在协程中打印出当时线程名。
import kotlinx.coroutines.*
fun main() {
    runBlocking {
        launch {
            val threadName = Thread.currentThread().name
            println("Current thread: $threadName")
        }
    }
}

在这个示例中,咱们运用runBlocking函数创立一个协程效果域,并运用launch函数来发动一个新的协程。在协程内部,咱们运用Thread.currentThread().name来获取当时线程的名称,并将其打印出来。
运转以上代码会输出类似于以下内容:
Current thread: main @coroutine#1
在这个示例中,当时线程的名称为main,而@coroutine#1是协程的标识符。请注意,由于协程是在调度器中履行的,因此能够在不同的线程上运转,这取决于所运用的调度器。
请保证在您的项目中添加kotlinx.coroutines库的依靠,以便能够运用协程功能。

  1. 经过协程下载一张网络图片并显现出来。
    我运用的是retrofit
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.ImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.http.GET
import retrofit2.http.Url
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalaesConverterFactory
import java.io.InputStream
interface ApiService {
    @GET
    suspend fun downloadImage(@Url imageUrl: String): String
}
val retrofit = Retrofit.Builder()
    .baseUrl("https://example.com/")
    .addConverterFactory(ScalarsConverterFactory.create())
    .build()
val apiService = retrofit.create(ApiService::class.java)
suspend fun downloadImageAndSetImageView(imageUrl: String, imageView: ImageView) {
    withContext(Dispatchers.IO) {
        val response = apiService.downloadImage(imageUrl)
        val bitmap = BitmapFactory.decodeStream(response)
        withContext(Dispatchers.Main) {
            imageView.setImageBitmap(bitmap)
        }
    }
}
//经过lifecycleScope.launch来发动一个协程
lifecycleScope.launch {
    downloadImageAndSetImageView(imageUrl, imageView)
}

版权声明

本文首发于:Kotlin 的协程用力瞥一眼 – 学不会协程?很或许由于你看过的教程都是错的

微信公众号:扔物线