协程在执行的过程,可以通过取消协程作用域来取消协程,这种情况下会抛出异常,但是已经被静默处理掉,因此用户无感知
1 不可取消的任务
在kotlin中,所有的挂起函数 withContext、delay等,都是可以被取消的,但是像CPU密集型任务,是不可被取消的,例如循环
var i = 0
val job = viewModelScope.launch {
//这里判断Job的生命周期
while (i < 5 /* && isActive ensureActive*/) {
i += 1
delay(1000)
Log.e("TAG", "index ---- $i++")
}
}
delay(2000)
Log.e("TAG", "我要取消了")
job.cancelAndJoin()
如果想要取消CPU密集型任务,因为在执行Job的cancel方法之后,isActive属性就是变为false,那么可以通过判断isActive 或者 ensureActive 来 break循环
2 协程上下文
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
在构建协程作用域中,第一个参数就是协程上下文CoroutineContext,默认就是EmptyCoroutineContext
2.1 协程上下文的组成
协程上下文是由Job,Dispatchers,CorotinueName,CorotinueExceptionHandler组成,其中Job用来控制协程的生命周期,Dispatchers用来分配协程执行的线程,CorotinueName代表协程的名字,在调试的时候,可以查看目标协程的状态,CorotinueExceptionHandler用来捕获协程中的异常
val job = viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
Log.e("TAG","${coroutineContext}")
}
打印:[CoroutineName(test), StandaloneCoroutine{Active}@6f35b52, Dispatchers.IO]
2.2 协程上下文的继承
子协程的创建,会创建一个新的Job对象,其他像Dispatchers,CorotinueName,都会继承自父协程
val job = viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
Log.e("TAG","${coroutineContext}")
//子协程
launch {
Log.e("TAG","${coroutineContext}")
}
}
打印: [CoroutineName(test), StandaloneCoroutine{Active}@6f35b52, Dispatchers.IO]
[CoroutineName(test), StandaloneCoroutine{Active}@edd0e23, Dispatchers.IO]
其中可以看出,除了Job对象不同之外,其他跟父协程一致,如果在子协程中声明新的上下文,那么就会覆盖
val job = viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
Log.e("TAG","${coroutineContext}")
//子协程声明了新的Dispatchers
launch (Dispatchers.Main){
Log.e("TAG","${coroutineContext}")
}
}
打印: [CoroutineName(test), StandaloneCoroutine{Active}@6f35b52, Dispatchers.IO]
[CoroutineName(test), StandaloneCoroutine{Active}@edd0e23, Dispatchers.Main]
3 异常的传播
当协程发生异常时,之前曾经介绍过的取消异常是被静默处理掉了,但是如果抛出其他的异常,需要手动处理异常
3.1 launch 和 async的异常捕获
launch和async是创建协程的2种方式,像launch创建的协程,异常是自动传播的,只要发生了异常,就会第一时间抛出;而async则是属于用户消费异常,只有用户调用await才能抛出异常
因此,launch和async在捕获异常的时候是不同的,launch需要在协程体内捕获异常,而async需要在调用await的时候,才能捕获异常
viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
print(123)
throw ArrayIndexOutOfBoundsException()
}
捕获异常的方式
viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
try {
print(123)
throw ArrayIndexOutOfBoundsException()
}catch (e:Exception){
if(e is ArrayIndexOutOfBoundsException){
Log.e("TAG","数组越界了")
}
}finally {
}
}
以上是launch方式创建的协程,如果是async创建的协程,在协程体内捕获异常是拿不到的
val job = viewModelScope.async {
delay(500)
throw IOException()
}
job.await()
捕获异常的方式
val job = viewModelScope.async {
delay(500)
throw IOException()
}
try {
job.await()
}catch (e:IOException){
Log.e("TAG","捕获到了异常")
}
当然,目前处理方式是在根协程中,如果是在子协程中,不管是launch还是async都会直接抛出异常,async都不需要await
viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
//这里try - catch 不到这个异常
async {
delay(500)
throw ArrayIndexOutOfBoundsException()
}
}
这里还没调用await,则直接抛出了异常,这是在子协程中
3.2 协程的传播特性
当子协程中发生异常时,首先会上报自己的父协程,父协程会取消自己全部的子协程,然后再取消自己,将异常上报到自己的父协程
这个过程中,通过try-catch是捕获不到异常的,异常还是会抛出而且上报到父协程,父协程会把这些协程全部取消,这就是launch和async无法捕获的异常,如果想要捕获这些异常,就得使用协程上下文中的CorotinueExceptionHandler
val handler = CoroutineExceptionHandler { _, throwable ->
//异常聚合 处理的异常会依附在上一个异常
Log.e("TAG","捕获了异常 ---- ${throwable.cause}")
}
viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") + handler ) {
try {
launch {
delay(500)
throw ArrayIndexOutOfBoundsException()
}
}catch (e:Exception){}
}
像子协程中抛出了异常,如果不加CoroutineExceptionHandler,那么就会取消全部的字协程,然后加上 CoroutineExceptionHandler 就能捕获这个异常,而且不会取消全部的子协程
除了上述的方式,可以通过SupervisorJob创建一个协程作用域或者使用supervisorScope创建协程作用域,这种方式我试过了,好像该是会取消父协程,我是在ViewModel中使用的,你们自己可私下试一下