由于协程在概念上关于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渠道的封装来合作运用)。
关于CoroutineScope
和CoroutineContext
的更多内后后边文章再讲。
协程最常用的功能是并发,而并发的典型场景便是多线程。能够运用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) {
...
}
练习题
- 敞开一个协程,并在协程中打印出当时线程名。
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库的依靠,以便能够运用协程功能。
- 经过协程下载一张网络图片并显现出来。
我运用的是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 的协程用力瞥一眼 – 学不会协程?很或许由于你看过的教程都是错的
微信公众号:扔物线