Bootstrap

Android Kotlin 协程详解

前言

关于Kotlin基础和高阶函数又不熟悉的可以先参考文章:

Android Kotlin 基础详解_袁震的博客-CSDN博客

Android Kotlin 高阶详解_袁震的博客-CSDN博客

什么是协程?要理解协程,就要将它和线程联系起来理解。

线程是什么?我想大家都清楚,而协程,它比线程更加轻量级,一个线程上面可以有多个协程。

如果我们应用开启一万个线程,可能就崩溃了,但是如果我们开启10万个协程,对应用的性能也不会有太大的影响。

协程是可挂起计算的实例,它需要一个代码块运行,并具有类似的生命周期(可以被创建、启动和取消),它不绑定到任何特定的线程,可以在一个线程中挂起其执行,并在另一个线程中恢复,它在完结时可能伴随着某种结果(值或异常)

协程的主要作用:

1,处理耗时任务,这种任务可能会阻塞主线程

2.,保证线程安全

一,协程的基本使用

Kotlin并没有将协程纳入标准库的API当中,而是以依赖库的形式提供的。所以如果我们想要使用协程功能,需要先在app/build.gradle文件当中添加如下依赖库:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
fun main() {

    GlobalScope.launch {
        //开启一个协程
        println("开启一个协程")
    }
}

注意:这段代码是没有输出的。后面会解释原因。 

二,协程的作用域 CoroutineScope

协程的作用域主要包括三种:

顶级作用域没有父协程的协程所在的作用域为顶级作用域。
协同作用域 协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
主从作用域 与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。

除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:
父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
子协程会继承父协程的协程上下文中的元素,如果自身有相同key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。

 

在kotlin中,所有的作用域都是CoroutineScope 的子类,下面是四种常用的作用域:

GlobalScope全局范围,不会自动结束执行。
MainScope主线程的作用域,全局范围
lifecycleScope生命周期范围,用于activity等有生命周期的组件,在Desroyed的时候会自动结束。
viewModeScopeViewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束

①GlobalScope:

 GlobalScope是全局的作用域,并且是无法取消的,因为:

public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

GlibalScope的源码可以看出,他的上下文对象是EmptyCorountineContext ,并没有Job对象,所以我们无法通过Job对象去cancle协程。

fun main() {

    GlobalScope.launch {
        //开启一个协程
        println("开启一个协程")
    }
}

这段代码为什么没有输出呢? 因为GlibalScope 是不阻塞线程的,主线程执行完了,此协程也会跟着结束,所以没有输出。

fun main() {

    GlobalScope.launch {
        //开启一个协程
        println("开启一个协程")
    }
    Thread.sleep(1000)
}

那如果我这样写,就会有输出打印 

②MainScope:

MainScope也是全局的作用域,但是它是可以取消的。

public fun MainScope(): CoroutineScope
 = ContextScope(SupervisorJob() + Dispatchers.Main)

它的上下文是 SupervisorJob()和主线程调度器构成的,所以它是可以取消的全局主线程协程。

MainScope是一个全局函数,我们可以在任何地方调用它(Activity,Fragment,Dialog,ViewModel等),但是需要注意在页面销毁的时候,需要手动cancle。

class MainActivity : AppCompatActivity() {

    var mScope=MainScope()
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mScope.launch { 
            println("执行了协程")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mScope.cancel()
    }
}

③ViewModelScope

ViewModelScope 是一个CloseableCoroutineScope,它的上下文由 SupervisorJob() + Dispatchers.Main.immediate构成,所以它也是可以取消的主线程协程

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
 
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }
 
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
 
    override fun close() {
        coroutineContext.cancel()
    }
}

可以看到 ViewModelScope是 ViewModel类的 扩展属性,假如这个  ViewModel 是 Activity 的,那么在 Activity 退出的时候,ViewModel 的  clear() 方法就会被调用,而  clear() 方法中会扫描当前 ViewModel 的成员  mBagsOfTags(一个Map对象)中保存的所有的  Closeable 的  object 对象(也就是上面的 CloseableCoroutineScope),并调用其  close() 方法。
所以当它用在ViewModel里面的时候,我们不用主动去回收它,它会自动回收。

④LifecycleScope:

LifecycleScope的实例是LifecycleCoroutineScopeImpl,它的上下文也是由SupervisorJob()+Dispatchers.Main.immediate构成所以它也是可以取消的主线程协程

public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
 
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

lifecycleScope是LifecycleOwner的扩展属性,因此它只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期。 它的基本使用和 viewModelScope 是一样的。但是它多了生命周期的一些感知。它也是通过  LifecycleController 中为 Lifecycle注册 观察者接口, 来感知 onResume的状态,然后进行调用的。

public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
    lifecycle.whenCreated(block)
}
public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
    lifecycle.whenStarted(block)
}
public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch { 
    lifecycle.whenResumed(block)
}

三,启动协程的方式

协程的启动,主要有五种方式:

调用  xxxScope.launch{...}  启动一个协程块, launch方法启动的协程不会将结果返回给调用方。

GlobalScope.launch {
    //开启一个协程
    println("开启一个GlobalScope协程")
}


在  xxxScope {...} 中调用  async{...} 创建一个子协程, async会返回一个 Deferred对象,随后可以调用 Deferred对象的 await()方法来启动该协程。

fun main() {

    GlobalScope.launch {
        //开启一个协程
        println("开启一个GlobalScope协程")
        val result =async {
            println("async")
            10
        }
      println("result:${result.await()}")
    }
    Thread.sleep(1000)
}

这里需要注意一下:async函数必须在协程作用域当中才能调用,await()是一个挂起函数,只能在挂起作用域内调用。所以通常不用async{}来创建最外层的协程,因为非挂起作用域无法调用await()函数获取协程的返回值。所以返回值没有意义,这样的话async()的返回值Deferred就是普通的Job,所以完全可以使用launch{}代替async{}


withContext(){...} 一个 suspend方法,在给定的上下文中执行并返回结果,它的目的不在于启动子协程,主要用于 线程切换,将长耗时操作从UI线程切走,完事再切回来。用它执行的挂起块中的上下文是当前协程的上下文和由它执行的上下文的合并结果。 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    mScope.launch {
        println("执行了协程")
        val result =withContext(Dispatchers.IO){
            println("切换到子线程")
            20
        }
        println(result)
        withContext(Dispatchers.Main){
            println("切换到主线程")
        }
    }
}

withContext()是顶级函数,可以直接调用(不需要创建协程对象)。但是withContext()是一个suspend挂起函数,它只能在协程或其他挂起函数中调用(必须先有协程)

调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回

withContext()函数强制要求我们指定一个线程参数,这个参数就是调度器,下面会讲


coroutineScope{...} &supervisorScope{...},创建一个新的作用域,并在该作用域内执行指定代码块,它并不启动协程。其存在的目的是进行符合结构化并发的并行分解。

private fun request() {
    lifecycleScope.launch {
        coroutineScope { // 协同作用域,抛出未捕获异常时会取消父协程
            launch { }
        }
        supervisorScope { // 主从作用域,抛出未捕获异常时不会取消父协程
            launch { }
        }
    }
}

coroutineScope  表示 协同作用域,  内部的协程 出现异常 会向外部传播,子协程未捕获的异常会向上传递给父协程,  子协程 可以挂掉外部协程 , 外部协程挂掉也会挂掉子协程,即 双向传播 。 任何一个子协程异常退出,会导致整体的退出。
supervisorScope 表示 主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程,内部的 子协程挂掉 不会影响外部的父协程和兄弟协程的继续运行,它就像一道防火墙,隔离了异常,保证程序健壮,但是如果外部协程挂掉还是可以取消子协程的,即 单向传播。它的设计应用场景多用于 子协程为独立对等的任务实体的时候,比如一个下载器,每一个子协程都是一个下载任务,当一个下载任务异常时,它不应该影响其他的下载任务。
 

⑤runBlocking{...} 创建一个协程,并阻塞当前线程,直到协程执行完毕。

fun main() {

    runBlocking {
        println("开启一个runBlocking协程")
    }
}

 一般的开发中我们尽量不使用这种方式,它通常用于main函数或者其他测试用例中,因为在main函数中启动一个协程去执行耗时任务,如果不阻塞main函数的线程,main函数执行完jvm就退出了,为了避免jvm退出,通常在最后需要Thread.sleep(Long.MAX_VALUE)让主线程休眠来等待协程执行完毕。但是如果使用runBlocking{}创建协程就不会出现jvm提前退出的问题。如在前面提到的不打印问题

四, 协程启动模式

在创建协程时,一般是有四种启动模式,如果我们不写的话,一般是默认的DEFAULT模式

①CoroutineStart.DEFAULT: 协程创建后,立即开始调度,但 有可能在执行前被取消。在调度前如果协程被取消,其将直接进入取消响应的状态。

mScope.launch (start =CoroutineStart.DEFAULT){
    println("执行了协程")
    val result =withContext(Dispatchers.IO){
        println("切换到子线程")
        20
    }
    println(result)
    withContext(Dispatchers.Main){
        println("切换到主线程")
    }
}

②CoroutineStart.ATOMIC  协程创建后,立即开始调度, 协程执行到第一个挂起点之前不响应取消。其将调度和执行两个步骤合二为一,就像它的名字一样,其保证调度和执行是原子操作,因此协程也 一定会执行

mScope.launch (start =CoroutineStart.ATOMIC){
    println("执行了协程")
    val result =withContext(Dispatchers.IO){
        println("切换到子线程")
        20
    }
    println(result)
    withContext(Dispatchers.Main){
        println("切换到主线程")
    }
}

③CoroutineStart.LAZY  只要协程被需要时(主动调用该协程的 start、 join、 await等函数时 , 才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。

mScope.launch (start =CoroutineStart.LAZY){
    println("执行了协程")
    val result =withContext(Dispatchers.IO){
        println("切换到子线程")
        20
    }
    println(result)
    withContext(Dispatchers.Main){
        println("切换到主线程")
    }
}

④CoroutineStart.UNDISPATCHED  协程创建后,立即在当前线程中执行,直到遇到第一个真正挂起的点。是立即执行,因此协程 一定会执行

mScope.launch (start =CoroutineStart.UNDISPATCHED){
    println("执行了协程")
    val result =withContext(Dispatchers.IO){
        println("切换到子线程")
        20
    }
    println(result)
    withContext(Dispatchers.Main){
        println("切换到主线程")
    }

五,协程调度器

官方框架中预置了 4 个调度器,我们可以通过  Dispatchers 对象访问它们

①Default: 默认调度器 ,适合处理后台计算,其是一个  CPU 密集型任务调度器

②IO: IO 调度器,适合执行 IO 相关操作,其是  IO 密集型任务调度器

③Main: UI 调度器,根据平台不同会被初始化为对应的 UI 线程的调度器, 例如在Android 平台上它会将协程调度到 UI 事件循环中执行,即通常在 主线程上执行。

④Unconfined:“无所谓“调度器,不要求协程执行在特定线程上。协程的调度器如果是 Unconfined, 那么它在挂起点恢复执行时会在恢复所在的线程上直接执行,当然, 如果嵌套创建以它为调度器的协程,那么这些协程会在启动时被调度到协程框架内部的事件循环上,以避免出StackOverflow。
 

如果创建 Coroutine的时候未指定调度器,或者使用未指定的调度器的上下文的 Scope通过 launch或 async启动一个协程,则默认是使用 Dispatchers.Default调度器 

由于 子协程会默认继承 父协程的 context上下文,所以一般我们可以直接为 父协程的 context上设置一个 Dispatcher,这样所有的子协程就自动使用这个 Dispatcher,当某个子协程有特殊需要的时候再其指定特定的 Dispatcher。
 

Default 和  IO 这两个调度器背后实际上是 同一个线程池。为什么二者在使用上会存在差异呢?由于 IO 任务通常会阻塞实际执行任务的线程,在阻塞过程中线程虽然不占用 CPU,  但却占用了大量内存,这段时间内被 IO 任务占据线程实际上是资源使用不合理的表现,因此 IO 调度器对于 IO 任务的并发做了限制, 避免过多的 IO 任务并发占用过多的系统资源,同时在调度时为任务打上 PROBABLY BLOCKING 标签,以方便线程池在执行任务调度时对阻塞任务和非阻塞任务区别对待。
 

六,suspend

在Kotlin协程中,被suspend修饰的函数是一个挂起函数,可以调用和使用协程库里的方法,仅能被suspend修饰的方法或lambda闭包使用,在其余地方使用会编译报错,因为被suspend修饰的函数再编译成java后会增加一个Continuation的参数,这个函数用于回调协程执行的结果,所以说协程的异步调用本质上就是一次异步调用。

当在协程中调用到挂起函数时,协程就会在当前线程(主线程)中被挂起,这就是协程中著名的非阻塞式挂起,主线程暂时停止执行这个协程中剩余的代码,注意:暂停并不是阻塞等待(否则会ANR),而是主线程暂时从这个协程中被释放出来去处理其他Handler消息,比如响应用户操作、绘制View等等。那挂起函数谁执行?这得看挂起函数内部是否有切换线程,如果没有切换线程当然就是主线程(当前线程)执行了,所以挂起函数不一定就是在子线程中执行的,但是通常在定义挂起函数时都会为它指定其他线程,这样挂起才有意义。

fun main(args: Array<String>) {
    runBlocking {
        println(Thread.currentThread().name)
        var html = getHtml()
        println(Thread.currentThread().name)
    }
}

suspend fun getHtml(): String {
    return GlobalScope.async {
        println(Thread.currentThread().name)
        delay(1000)
        URL("https://www.baidu.com").readText()
    }.await()
}

比如上面的例子,在主线程(默认线程)中启动了一个协程,当执行到getHtml()时,切换到了GlobalScope协程中(默认运行在工作线程中)去执行,此时主线程中的代码将被暂时挂起。当getHtml中的方法执行完并返回后,主线程中的协程才会继续运行,这叫做协程恢复,如果遇到了其他挂起函数还会重复这个过程。

;