文章目录
前言
通过前一篇协程-Android端使用场景入门,了解协程的一些使用场景,为了能更高效的使用协程,我们还需要了解知识,这样才能在使用过程中得心应手。
为了能够更好的理解后面的内容,这里针对一些概念做统一的说明
(1)协程层级划分
- CoroutineScope称为RootParent,同时也是parent
- CoroutineScope启动的协程称为RootParent的child,同时协程里面又可以启动协程,此时该层也叫parent
- 协程可以看做一种层级结构,统一概念是为了后面提到child或者parent的时候能够更好的明白
(2)协程生命周期
协程有上述的几种状态,其中的部分状态可通过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句柄,通过它可以控制协程的生命周期,协程的生命周期如下图所示:
开发中,能够访问的属性有:isActive、isCancelled、isCompleted。
如果一个协程处于active状态,当失败或者调用job.cancel(),Job会进入Cancelling状态(isActive = false,isCancelled = true)。当所有协程的的工作完成后,进入Cancelled状态,isCompleted = true.
四、了解异常
在协程中,当出现异常的时候,会将异常传递到其父级,一般情况父级按下三种处理方式:
- 结束其所有的子协程
- 父级自己结束
- 向上一级传递
如下图所示:
这种情况下,异常会到协程的顶级RootParent,即CoroutineScope级,并且该CoroutineScope启动的协程都会被结束。
如果我们一个UI中使用scope启动多个协程,很显然不希望如果其中的一个协程抛出了异常,该scope所有的子协程都被取消,在这种场景下,可以使用SupervisorJob构建CoroutineContext。
五、SupervisorJob
5.1 概念
使用SupervisorJob,如果有一个子协程出错,不会影响其他的子协程.简单来讲就是不会影响CoroutineScope所启动的其他协程。如下图所示:
我们可以按以下方式使用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
最后
上面这些内容介绍协程在使用中的一些注意事项,主要分为以下内容:
- CroutineScope和CoutineConext的作用
- Job和SupervisorJob的区别
- 在不同模式下如何正确的处理协程异常
- 如何有效的停止协程工作
参考资料:
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