Kotlin 协程-基础篇

116 阅读3分钟

一、前言

本期是协程的基础篇,会从协程作用域、上下文、启动模式等方面讲述如何去使用协程。并且,会基于协程提供的框架,封装工作中常用的基础能力。

二、协程作用域

初学协程的时候,就听说过挂起函数必须在另一个挂起函数中或者协程作用域中才能被调用。那么,协程作用域是什么?如何创建一个协程作用域?

fun main() {
    GlobalScope.launch { }
}

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

实现了 CoroutineScope 的类,创建该类的对象,就会得到一个协程作用域。

GlobalScope 是协程框架提供的一个全局作用域,由于它是静态的,因此它的生命周期一直持续到应用程序终止。在Android领域中,Google 提供一个一系列具有生命周期感知的 CoroutineScope。

  1. LifecycleScope:能监听页面的生命周期,在页面销毁的时候,取消协程
  2. ViewModelScope:能够和 ViewModel 绑定,在 ViewModel 销毁的时候取消协程
  3. MainScope:作用在主线程的协程作用域能够在这里进行 UI 的更新

三、协程上下文

协程上下文是一系列的实现了 Element 的集合,包括了 Job、CoroutineExceptionHandler、Dispatchers等。

CoroutineContext = Job() + CoroutineName("") + CoroutineExceptionHandler { coroutineContext, throwable -> } + Dispatchers.IO

另外,协程上下文也是可以进行自定义的,只需要继承 Element 就可以了,示例如下:

class UserCoroutineContext(val id: String) : CoroutineContext.Element {
    override val key = KEY

    companion object KEY : CoroutineContext.Key<UserCoroutineContext>
}

四、协程启动模式

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi // Since 1.0.0, no ETA on stability
    ATOMIC,
    UNDISPATCHED;
}

我们通常用 DEFAULTLAZY 的场景比较多,这四种启动模式的很好理解,解释如下:

  1. DEFAULT:默认的启动模式,协程创建完成之后就开始调度
  2. LAZY:懒加载模式,例如 async 之后不直接启动,而是在 await 之后才开始调度
  3. ATOMIC:启动之前不可取消
  4. UNDISPATCHED:直接基于当前线程调用,直到遇到第一个挂起点,才会切换到 Dispatchers 指定的线程进行调度

五、协程调度器

public actual object Dispatchers {
    @JvmStatic
    public actual val Default: CoroutineDispatcher = DefaultScheduler

    @JvmStatic
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultIoScheduler
}

协程的调度方式分为 4 种,解释如下:

  1. Default:处理 CPU 密集型的任务
  2. Main:主线程执行
  3. Unconfined:不指定线程,直到遇到第一个挂起点,挂起恢复之后,切换到默认的线程
  4. IO:IO 线程执行

六、协程异常处理

在一个协程作用域内,启动了多个协程,如果某个协程失败,那么其它的协程也会受到影响被取消,如下图实例:

image.png

如果要让子协程之间不会相互影响,可以使用 SupervisorJob,示例如下:

fun main() {
    runBlocking {
        GlobalScope.launch {
            val job1 = async(SupervisorJob()) {
                println("job1")
                delay(1000)
                error("错误!!!!")
            }
            val job2 = async() {
                delay(2000)
                println("job2")
            }
            job2.await()
            job1.await()
        }.join()
    }
}

日志输出如下:

job1
job2
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalStateException: 错误!!!!
	at com.xxxx.xxxx.xxxx.xxxxx.viewmodels.AKt$main$1$1$job1$1.invokeSuspend(A.kt:42)

job1 的失败不会影响 job2 的输出,但是应用程序依然会发生 Crash。SupervisorJob 能够阻止异常的传播,但是抛出的异常仍然需要被正确处理才行。示例如下:

fun main() {
    runBlocking {
        val handle = CoroutineExceptionHandler { coroutineContext, throwable ->
            println(throwable.message)
        }
        GlobalScope.launch(handle) {
            val job1 = async(SupervisorJob() ) {
                println("job1")
                delay(1000)
                error("错误!!!!")
            }
            val job2 = async() {
                delay(2000)
                println("job2")
            }
            job2.await()
            job1.await()
        }.join()
    }
}