协程-在使用过程需要了解一些内容

前言

通过前一篇协程-Android端使用场景入门,了解协程的一些使用场景,为了能更高效的使用协程,我们还需要了解知识,这样才能在使用过程中得心应手。
为了能够更好的理解后面的内容,这里针对一些概念做统一的说明

(1)协程层级划分

image

  • CoroutineScope称为RootParent,同时也是parent
  • CoroutineScope启动的协程称为RootParent的child,同时协程里面又可以启动协程,此时该层也叫parent
  • 协程可以看做一种层级结构,统一概念是为了后面提到child或者parent的时候能够更好的明白

(2)协程生命周期

image

协程有上述的几种状态,其中的部分状态可通过Job进行访问,后续会进一步说明。

一、CoroutineScope

我们可以称为:协程作用域,就是对一个协程定义一个作用域,通过该作用域来启动或者控制协程的生命周期。在Android的KTX库中,已经提供了一些包含生命周期的CoroutineScope,viewModelScope和lifecycleScope,这两个的使用可以参考协程-Android端使用场景入门

同时我们也可以自己创建CoroutineScope,并在合适的时候控制它的生命的周期。如下所示:

//创建协程作用域
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

//创建协程-job是协程的句柄,可以控制该协程的生命周期,比如job.cancel()结束当前协程
val job = scope.launch{
    
}

//结束当前协程
job.cancle()

//结束整个CoroutineScope,通过scope.launch或者scope.async()创建的协程都会cancel
scope.cancel()

CoroutineScope创建很简单,传入一个CoroutineContext即可。下面是CoroutineScope的源码定义,

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

这个接口就只包含一个属性CoroutineContext,平时我们在开发使用到的都是CoroutineScope的扩展方法或者扩展属性。比如launch(),async(),cancel(),ensureActive()等,所以在创建CoroutineScope的时候,更重要的是关心CoroutineContenxt。下面一起来了解一下CoroutineContext。

二、CoroutineContext

协程的上下文,由不同的元素组成,分别定义协程的不同行为。包含以下元素:

  • Job - 控制协程的生命周期
  • CoroutineDispatcher - 将工作分配给合适的线程
  • CoroutineName - 协程的名字,调试的时候有用
  • CoroutineExceptionHandler - 处理未捕获的异常

当我们创建一个协程的时候,它的CoroutineContext是什么呢?可以是自己的父CoroutineScope的CoroutineContext,也可以是一个协程的CoroutineContext。如下:

val scope = CoroutineScope(Job() + Dispatchers.Main)

val job = scope.launch {
    //该协程使用scope作为的父
    val result = async {
        // New coroutine that has the coroutine started by 
        // launch as a parent
    }.await()
}

三、协程生命周期

当我们通过scope.launch或者scope.async启动协程的时候,返回一个协程的Job句柄,通过它可以控制协程的生命周期,协程的生命周期如下图所示:

image
开发中,能够访问的属性有:isActive、isCancelled、isCompleted。

如果一个协程处于active状态,当失败或者调用job.cancel(),Job会进入Cancelling状态(isActive = false,isCancelled = true)。当所有协程的的工作完成后,进入Cancelled状态,isCompleted = true.

四、了解异常

在协程中,当出现异常的时候,会将异常传递到其父级,一般情况父级按下三种处理方式:

  1. 结束其所有的子协程
  2. 父级自己结束
  3. 向上一级传递

如下图所示:

image

这种情况下,异常会到协程的顶级RootParent,即CoroutineScope级,并且该CoroutineScope启动的协程都会被结束。

如果我们一个UI中使用scope启动多个协程,很显然不希望如果其中的一个协程抛出了异常,该scope所有的子协程都被取消,在这种场景下,可以使用SupervisorJob构建CoroutineContext。

五、SupervisorJob

5.1 概念

使用SupervisorJob,如果有一个子协程出错,不会影响其他的子协程.简单来讲就是不会影响CoroutineScope所启动的其他协程。如下图所示:
image

我们可以按以下方式使用SupervisorJob

val uiScope = CoroutineScope(SupervisorJob())

Android官方KTX库中的viewModelScope和lifeCycleScope都是使用的SupervisorJob,参代码如下:

public 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)
        )
    }


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
            }
        }
    }

或者下面的方式:

val scope = CoroutineScope(SupervisorJob())
scope.launch {
    // Child 1
}
scope.launch {
    // Child 2
}
val scope = CoroutineScope(Job())
scope.launch {
    supervisorScope {
        launch {
            // Child 1
        }
        launch {
            // Child 2
        }
    }
}

注意:SupervisorJob需要放在CoroutineScope的CoroutineContext或者使用
supervisorScope才会生效。比如下面代码:

val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
    // new coroutine -> can suspend
   launch {
        // Child 1
    }
    launch {
        // Child 2
    }
}

在上面的片段代码中,Child1和Child2的job都是Job()而不是SupervisorJob()。如果其中的一个child出现未捕获的异常,那么该scope会被全部取消。

5.2 SupervisorJob和Job的异常验证

使用Job,代码如下:

    private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        println("Caught $throwable")
    }
    private fun fetchRemoteData() {
        throw Exception("fetch data error")
    }
    private fun testJob() {
        val scope = CoroutineScope(Job() + Dispatchers.IO)
        val job1 = scope.launch(exceptionHandler) {
            // child1
            fetchRemoteData()
        }
        val job2 = scope.launch {
            // child2
            delay(1000)
            println("this is child2 coroutine execute")
        }

    }

运行testJob方法,输出日志如下:

I/System.out: Caught java.lang.Exception: fetch data error

这里child2的println方法没有执行。下面我们修改创建scope的方法,将参数Job换成SupervisroJob()

    private fun testJob() {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
        val job1 = scope.launch(exceptionHandler) {
            // child1
            fetchRemoteData()
        }
        val job2 = scope.launch {
            // child2
            delay(1000)
            println("this is child2 coroutine execute")
        }

    }

再次运行testJob(),日志如下:

 I/System.out: Caught java.lang.Exception: fetch data error
 I/System.out: this is child2 coroutine execute

这里我们看到child2执行了,经过上面的两次代码验证,得出Job和SupervisorJob的区别:

  • 使用SupervisorJob,如果有子协程抛出异常,协程不会向上传递,自己处理,也不影响scope以及其他子协程
  • 使用Job,如果有子协程抛出异常,父级结束所有子协程,父级自己结束,如果存在上一级,则继续线向上一级传递

六、协程中该如何处理异常

异常的处理和Kotlin的使用一样,使用try/catch或者CoroutineExceptionHandler

我们知道使用SupervisorJob和Job,内部对异常的处理方式不同,开发中我们用德比较多的是SuervisorJob,因此下面我们先了解SupevisroJob模式的异常处理。

而在协程中,不同的协程构建方式,对异常的处理有不同的方式。下面分别说明launch和async的方式

6.1 Launch

通过launch启动的协程,当出现异常时,异常会及时抛出。我们可以按照一下方式处理异常。

scope.launch {
    try {
        codeThatCanThrowExceptions()
    } catch(e: Exception) {
        // Handle exception
    }
}

6.2 Async

当使用async作为根协程(即:协程是CoroutineScope或者supervisorScope实例的直接子级),异常不会立即抛出,当调用await()方法使时才抛出。针对这种异常,我们对await()方法进行try/cathc即可:

supervisorScope {
    val deferred = async {
        codeThatCanThrowExceptions()
    }
    try {
        deferred.await()
    } catch(e: Exception) {
        // Handle exception thrown in async
    }
}

下面我们了解Job模式下的异常发生情况,如下代码:

    private fun testJob3() {
        val scope = CoroutineScope(Job() + Dispatchers.IO)

        scope.launch {
            try {
                val deferred = async {
                    fetchRemoteData()
                }
                deferred.await()
            } catch (ex: Exception) {
                //async中的异常不会到达这里,会向上传递
                ex.printStackTrace()
            }
        }
    }

运行该方法,你会发现,程序会崩溃,try/cathc并没有捕获到async中抛出的异常。因此使用Job方式,异常会直接向上一层级抛出。而向上一级抛出的异常,我们通过CoroutineContext中的CoroutineExceptionHandler来处理。如下所示:

    private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        println("Caught $throwable")
    }
    val scope = CoroutineScope(Job() + Dispatchers.IO)
    scope.launch(exceptionHandler) {
    val deferred = async {
        fetchRemoteData()
    }
    deferred.await()
    }
    //或者传递给scope的CouroutineContext
    val scope = CoroutineScope(Job() + Dispatchers.IO+exceptionHandler)
    scope.launch {
    val deferred = async {
        fetchRemoteData()
    }
    deferred.await()
    }

按照上面的方式,程序正常运行,因为我们通过我们CoroutineExceptionHandler处理了协程中未捕获的异常,

而下面的这种方式是不能处理未捕获的异常的:

    private fun testJob3() {
        val scope = CoroutineScope(Job() + Dispatchers.IO)
        scope.launch {
            launch(exceptionHandler) {
                fetchRemoteData()
            }
        }
    }

这里异常就未能被捕获处理,因为job()类型的异常向上传递后,没有在对应的CoroutineContext中找到对应的exceptionHandler

七、Cancel coroutine 取消协程

当我们启动多个协程时,可以通过协程作用域(scope)来取消他们:

val job1 = scope.launch{ ...... }
val job2 = scope.launch{ ...... }

//取消该scope下的所有协程
scope.cancel()

某些情况下我们想取消其中的一个协程,可进行如下操作:

val job1 = scope.launch{ ...... }
val job2 = scope.launch{ ...... }

//取消job1
job1.cancel()

注意:这里取消子协程需要留意Job()或者SuperiseJob()下的不同。如果调用了scope.cancel(),那么该scope将不能启动新的协程。

在androidx 的KTX扩展库中,我们可以根据具体的需求来选择使用viewModelScope或者LifecycleScope,这两个scope都会在合适的时机来结束scope。比如viewModleScope会在ViewModel的clear()方法中取消对应的scope.

7.1 cancle后协程会立即结束吗?

如果我们仅仅调用cacel()方法,并不意味着协程的工作会立即结束。比如下面的例子:

    private fun testCancel() {
        val scope = CoroutineScope(Job() + Dispatchers.IO)
        val job = scope.launch {
            val startTime = System.currentTimeMillis()
            var nextPrintTime = startTime
            var i = 0
            while (i < 5) {
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("hello ${i++}")
                    nextPrintTime += 500
                }
            }
        }
        scope.launch {
            delay(1000)
            println("start cancel")
            job.cancel()
        }
    }

上面程序每隔500毫秒打印一次,那么调用1秒过后调用job.cancel(),启动的协程还会继续打印吗?

运行我们的程序,结果如下:

 2100-2136/com.persson.myapplication I/System.out: hello 0
 2100-2136/com.persson.myapplication I/System.out: hello 1
 2100-2136/com.persson.myapplication I/System.out: hello 2
 2100-2138/com.persson.myapplication I/System.out: start cancel
 2100-2136/com.persson.myapplication I/System.out: hello 3
 2100-2136/com.persson.myapplication I/System.out: hello 4

当调用job.cancle()后,协程进入Cancelling状态,但协程会继续运行,打印hello 3和hello 4,当所有协程的工作完成后,协程进入Cancelled状态。

7.2 如何正确的停止协程的工作

在上述的示例中,为了保证cancel后协程能够停止所有的工作,以及进入canceld状态,我们可以在循环中使用job.isActive或者ensureActivie()判断
如下:

    private fun testCancel() {
        val scope = CoroutineScope(Job() + Dispatchers.IO)
        val job = scope.launch {
            ...
            var i = 0
            while (i < 5 && isActive) {
               ...
            }
        }
        ...
    }
    //或
    private fun testCancel() {
    val scope = CoroutineScope(Job() + Dispatchers.IO)
    val job = scope.launch {
        ...
        var i = 0
        while (i < 5 &&) {
            ensureActive()
           ...
        }
    }
    ...
}
    

运行结果如下:

 2100-2136/com.persson.myapplication I/System.out: hello 0
 2100-2136/com.persson.myapplication I/System.out: hello 1
 2100-2136/com.persson.myapplication I/System.out: hello 2
 2100-2138/com.persson.myapplication I/System.out: start cancel

最后

上面这些内容介绍协程在使用中的一些注意事项,主要分为以下内容:

  1. CroutineScope和CoutineConext的作用
  2. Job和SupervisorJob的区别
  3. 在不同模式下如何正确的处理协程异常
  4. 如何有效的停止协程工作

参考资料:

https://medium.com/androiddevelopers/coroutines-first-things-first-e6187bf3bb21

https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629

https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值