Kotlin协程-CoroutineContext协程上下文

2,973 阅读6分钟

Kotlin协程-CoroutineContext上下文

Kotlin协程系列:

Context 上下文,我们都知道了,在 Android 中可以调用资源,启动四大组件,获取系统服务,等等。而 CoroutineContext 协程上下文则是可以控制协程,调度协程,捕获异常等。

协程总是运行在 CoroutineContext 类型的上下文环境中的。

前文中我们得知,启动/创建一个协程有三种方法

launch async runBlocking 我们查看他们的源码可以看到第一个参数都是需要传入一个协程上下文,当然默认的都是EmptyCoroutineContext。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
public fun <T> runBlocking(context: CoroutineContext
 = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {

下面就讲解几个常用的上下文实现类看看如何使用

一、调度器 Dispatchers

我们可以通过父协程指定运行的线程,内部的子协程也可以指定到具体的线程

  CoroutineScope(Dispatchers.Main).launch {

           GlobalScope.launch(Dispatchers.Default) {
                YYLogUtils.w("执行在另一个协程中...")

                delay(1000L)

                YYLogUtils.w("另一个协程执行完毕...")
            }

            val deferred = async(Dispatchers.IO) {
                YYLogUtils.w("切换到另一个协程")
                Thread.sleep(2000)
                return@async "response data"
            }

            val response = deferred.await()
            YYLogUtils.w("response:$response")

            runBlocking(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                YYLogUtils.w("result1:1234")
            }

            withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                YYLogUtils.w("result2:123456")
            }
    }

可以看到不管是指定协程的运行线程,还是临时切换线程,运行完毕会切换回来,都是通过 Dispatchers 来调度的。

常用的线程调度为:

  • Dispatchers.Main Android主线程
  • Dispatchers.Unconfined 当前CoroutineScope的线程策略
  • Dispatchers.Default 默认值,为JVM共享线程池
  • Dispatchers.IO IO线程池,默认为64个线程

当然除了系统自带的调度器 Dispatchers ,我们还能通过一些扩展方法实现自己的调度器,让协程运行在我们指定的线程上。

比如我们想要运行在 HandleThread 线程,或者 Executors 线程池中的线程,我们可以这样做:

    private var mHandlerThread: HandlerThread? = HandlerThread("handle_thread")

    private var mHandler: Handler? = mHandlerThread?.run {
        start()
        Handler(this.looper)
    }

    ...

    GlobalScope.launch(mHandler.asCoroutineDispatcher("handle_thread")) {

        YYLogUtils.w("执行在协程中...")

        delay(1000L)

        YYLogUtils.w("执行完毕...")
    }

这样就可以运行在指定的HandleThread线程中。

同样的我们可以创建一个线程池,比如我创建一个单线程池,那么把它转换为一个协程上下文的调度Dispatcher对象,那么这个协程就会运行在我们指定的线程池中的线程上。

GlobalScope.launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {

    YYLogUtils.w("执行在协程中...")

    delay(1000L)

    YYLogUtils.w("执行完毕...")
}

二、CoroutineName 协程命名

如果一个协程中有多个子协程,如果都张的一样,我想知道谁是谁,我们就可以通过 CoroutineName 来命名,它也是 CoroutineContext 的实现类,使用起来非常的简单,直接构造即可:

GlobalScope.launch(CoroutineName("parent")) {

        async(CoroutineName("child1")) {
            YYLogUtils.w("切换到另一个协程1")
            Thread.sleep(2000)
            return@async "response data"
        }.await()

        async(CoroutineName("child2")) {
            YYLogUtils.w("切换到另一个协程2")
            Thread.sleep(2000)
            return@async "response data"
        }.await()

}

三、Job 协程控制

Job 不是我们协程启动返回的对象吗?用于cancel join 等操作。它虽然是协程启动的返回值,但是同样也是 CoroutineContext 的实现类,一样可以用于启动协程的构造中。

例如我们之前是这么处理的:

var job: Job? = null

...

job = GlobalScope.launch(Dispatchers.Main) {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    YYLogUtils.w("执行完毕...")
}


override fun onDestroy() {
    job?.cancel()
    super.onDestroy()
}

我们之前都是这么处理的,例如 launch 的时候返回一个Job,然后通过成员变量在页面退出的时候取消掉这个协程的运行,没毛病。

如果使用构造方法,我们就可以这么操作了:

var job: Job? = null

...

 GlobalScope.launch(job.run { Job() }) {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    YYLogUtils.w("执行完毕...")
}


override fun onDestroy() {
    job?.cancel()
    super.onDestroy()
}

或者更简单的,直接就初始化就构造好:

var job: Job = Job()

...

 GlobalScope.launch(job) {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    YYLogUtils.w("执行完毕...")
}


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

效果和上面使用返回值的方式一样的,都能在 onDestroy 的时候取消协程。

四、CoroutineExceptionHandler 异常处理

协程的异常处理我们大家都会,不就是try catch嘛,简单

 GlobalScope.launch() {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    val num = 999/0

    YYLogUtils.w("执行完毕...")
}

这样都会报错的,如果你没有处理,那么协程可不会自动帮你处理,你的App就会崩溃。我们可以try catch处理即可:

 GlobalScope.launch() {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

      try {

         val num = 999/0

        } catch (e: Exception) {
            e.printStackTrace()
        }
 
     YYLogUtils.w("执行完毕...")
}

那其实我们传入一个处理异常的上下文对象,一样能实现这个效果,就无需在协程中到处try catch了。

private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    YYLogUtils.e(throwable.message ?: "Unkown Error")
}

 GlobalScope.launch(exceptionHandler) {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    val num = 999/0

    YYLogUtils.w("执行完毕...")
}

这样我们就可以在外部统一的处理异常信息了。

基于这样的处理,我们还能做一个简单的扩展方法,让协程安全的启动,定义的扩展方法如下:

fun CoroutineScope.safeLaunch(
    onError: ((Throwable) -> Unit)? = null,
    onLaunchBlock: () -> Unit
) {
    val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        onError?.invoke(throwable)
    }

    this.launch(exceptionHandler) {
        onLaunchBlock.invoke()
    }
}

我们就能安全的启动协程啦,使用如下:

 GlobalScope.safeLaunch(onError = { throwable ->

   YYLogUtils.w(throwable.message ?: "UnKnow Error")

 })  {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    val num = 999/0

    YYLogUtils.w("执行完毕...")
}

当然了这只是最简单的封装,在实际开发中,我们应该还需要传入协程上下文等参数继续丰富这个扩展函数。

五、组合协程上下文

这么多好用的功能,我看每次都只能传入一个上下文对象,我能不能都要?答案是可以的。

通过CoroutineContext.plus方法,我们可以组合定义多个上下文元素,我们可以使用 + 操作符来实现。

例如我把上面的几个上下文都组合起来看看

var job: Job = Job()

private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    YYLogUtils.e(throwable.message ?: "Unkown Error")
}

 GlobalScope.launch(Dispatchers.Main + job + exceptionHandler + CoroutineName("job") 
 + Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    val num = 999/0

    YYLogUtils.w("执行完毕...")
}

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

Ok,这样 + 起来,我们就可以实现全部的功能全部集成在一起,是不是很方便呢。

总结

上一篇我们讲了协程的基本使用,掌握的就是协程启动的几种方式,切换线程的几种方式,异步与同步的执行,和挂起函数,阻塞与非阻塞的概念。

这一篇我们进一步理解切换线程调度器原来是基于协程上下文实现,还掌握了常用的几种协程上下文处理。

下一篇我们会讲一下协程作用域的概念,与如何自定义一个自己的协程作用域。

协程的概念与框架比较大,我本人如有讲解不到位或错漏的地方,希望同学们可以指出交流。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿