协程( Coroutines)并不是 Kotlin 提出来的新概念,很多的编程语言都有实现,如:Go、Python 等。。本文所讲,专指kotlin的协程。
在Android 11中,Asynctask已经被废弃了,因为协程可以更简单,直观的实现异步任务。而且协程是谷歌推荐的异步处理机制,那么什么是协程呢?其实很简单,就是kotlint封装的一套线程api(线程框架),类似于 Java 中的 Executor 和 Android &Java中的 AsyncTask、Handler。
协程的创建方式有三种,async{},launch{}和runBlocking{}。如下:
launch{ getName() }
var name = async{ getName() }
Log.i(TAG, "name1 is " + name.await())
runBlocking{ getName() }
runBlocking的方式,在开发中基本不会用到,因为它是线程阻塞的。launch是最常用的,async有时也会用到。用async创建,会有一个 Deferred 类型的返回值,需要使用 返回值.await()来获取返回值,如果不用,不等async返回,就执行下面的打印语句了。
但是launch和async不可以直接使用,需要在协程作用域里面才能用,有下面三种方式创建协程作用域。
runBlocking {
getName()
}
//通过使用 GlobalScope 单例的方式
GlobalScope.launch {
getName()
}
// 参数类型为 CoroutineContext ,Dispatchers.Main 和 Job 都是CoroutineContext 类型
val scope = CoroutineScope(Dispatchers.Main + Job())
//取消协程:job.cancle() 判断状态:job.isActive job.isCancelled isCompleted
val job = scope.launch {
getName()
}
runBlocking的方式上面说过了,方法二不会阻塞线程,但是也不推荐这种用法,因为它的生命周期和 app 一样,且不能取消,推荐使用第三种方式。来看下CoroutineScope的参数。
Dispatcher 用于告知协程,应该在哪个线程中运行。
Dispatchers.Main 就是在 Android 的主线程中运行,除了 Main 之外,我们还可以指定:
Dispatchers.IO:对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,如:读写文件,操作数据库和网络请求
Dispatchers.Default:适合 CPU 密集型的任务,比如运算
Job是协程的唯一身份标识,可以用来控制和判断协程的状态。
下面来具体看看协程到底有多简洁,方便。(注:launch{}代码块就是协程)
val scope = CoroutineScope(Job()+Dispatchers.Main)
val job1 = scope.launch {
// xxxxxxxxx location 1 运行在主线程
var name = getName() // location 2 运行在IO线程
var pwd = getPwd() // location 3 运行在IO线程
textView1 = name //location 4 运行在主线程
textView2 = pwd
}
private suspend fun getName() = withContext(Dispatchers.IO) {
// delay(4000)
val name: String = getNameFromNetwork();//耗时操作
name
}
private suspend fun getPwd() = withContext(Dispatchers.IO) {
// delay(4000)
val pwd: String = getPwdFromNetwork();//耗时操作
pwd
}
以前进行网络操作时,基本都是用的回调,有了协程,我们就可以直接获取,然后直接使用。而且不同的线程可以写在一处,这就是神奇的协程!
有几处需要说明一下:
withContext()是kotlin内置的suspend(挂起函数),可以指定它所包含的代码块运行在哪个线程,很常用的,代码块的最后一行就是返回值。
getName()是自定义的挂起函数,需要在挂起函数或者协程中调用。
说下这段代码的执行流程:
当执行到location 1时,运行在主线程
当执行到location 2时,就兵分两路了,一路是主线程,一路是协程(此处是IO线程)
主线程跳出协程,也就是跳出launch{}代码块,执行后续代码
协程(IO线程)就可以执行它的耗时的动作了
IO线程执完毕时,切回到主线程,执行location 4
所以协程的本质就是启动一个新线程,然后执行新的线程,也就是suspend函数(不影响主线程,主线程该干嘛干嘛),等新的线程执行完毕,再切回(合并)到主线程,也就是resume。
为什么suspend函数直接或者间接的被协程调用呢?这是因为切换回主线程的动作只有协程才能做。这么看来,suspend函数的作用只是切换到IO线程咯,没那么简单!在getName()函数中也可以写withContext(Dispatchers.Main),那就不用切线程了,那么suspend函数到底有什么用呢?其实就是一个提醒,suspend函数的创建者提醒调用者–本函数可能会有耗时操作,需要在协程里面调用。
为什么都说suspend函数是非阻塞式挂起呢?道理很简单,因为挂起不影响主线程啊,只不过是开启了一个新的线程做耗时操作而已。需要说明的是,自定义 的suspend函数,需要调用系统自带的挂起函数,如果不调用,那么它就没有存在的意义了。
其实上面的代码还有改进的空间,getPwd()是要等getName()执行完毕才会接着执行,因为它俩运行在同一个线程,这样显然是有些浪费时间的,所以可以进行如下改造:
var name = async{ getName() } // location 2 运行在IO线程
var pwd = async{ getPwd() } // location 3 运行在IO线程
textView1 = name.await() //location 4 运行在主线程
textView2 = pwd.await()
这样在协程内部又创建了两个子协程,如此一来,getPwd()和getName()就运行在两个不同的线程,而且使用了await()就是等asuync{}返回了再去赋值。
协程内部再创建协程,那么新创建的协程就是原来协程的子协程,举例说明:
scope = CoroutineScope(Job()+Dispatchers.Main)
val job1 = scope.launch { // No.1
// xxxxxxx location 1
var name = getName()
// xxxxxxx location 2
var job2 = launch{ // No.2
var jo4 = launch{ // No.4
}
}
var job3 = launch{ // No.3
}
}
private suspend fun getName() = withContext(Dispatchers.IO) {
if(!scope.isCancelled) { // ensureActive()
// delay(4000)
val name: String = getNameFromNetwork();//耗时操作
name
} else {
""
}
}
上面的代码中一个创建了4个协程,No.2 No.3的父协程是No.1,它俩是兄弟协程,No.4的父协程是No.2。协程的这种特性叫做结构化并发,这个特性,使得管理起来很是方便,比如,取消所有协程,只需要调用:scope.cancel()就可以了。要取消No.2 No.4,只需要调用 job2.cancel()。取消操作是取消自己和它的子协程。
如果协程正在执行时被取消了,拿上面的例子来说,如果执行到location 1,协程被取消,那么后面的都不执行了,如果正在执行getName()那么会执行完该函数,后面的不再执行,等getName()执行完毕,系统还会再自动检测协程是否完成(withContext的功能),如果完成了,后面的也不执行了。
kotlin官方有这么一个对比:
repeat(100000) {
launch {
delay(1000)
println(".")
}
}
repeat(100000){
Thread{
Thread.sleep(1000)
println(".")
}
}
开启十万个协程与开启十万个线程对比,协程可以正常执行,而线程却出现了内存溢出,官方以此来说明协程比线程更加轻量级,其实这是不对的,因为协程是开启了一个线程池来运行线程的,所以正确对比应该是下面这样:
val executors = Executors.newSingleThreadScheduledExecutor()
var task = Runnable{
println(".")
}
repeat(100000) {
executors.schedule(task, 1, TimeUnit.SECONDS)
}
协程 VS 线程池,性能不相上下。
最后再来说下使用协程所需要的依赖吧!
根目录下的 build.gradle :
buildscript {
...
ext.kotlin_coroutines = '1.3.1'
...
}
Module 下的 build.gradle :
dependencies {
...
// 依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
// 依赖当前平台所对应的平台库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
...
}
好了,到这里,kotlin的协程就讲的差不多了,来总结一下,kotlin协程就是一个线程框架,它可以启动并且切换线程,而且在线程执行完毕后,还可以再切回来,代码看起来很简洁,用阻塞式的代码实现了非阻塞的效果。