協(xié)程中的異常處理
Parent-Child關(guān)系
如果一個(gè)coroutine拋出了異常, 它將會(huì)把這個(gè)exception向上拋給它的parent, 它的parent會(huì)做以下三件事情:
- 取消其他所有的children.
- 取消自己.
- 把exception繼續(xù)向上傳遞.
這是默認(rèn)的異常處理關(guān)系, 取消是雙向的, child會(huì)取消parent, parent會(huì)取消所有child.
catch不住的exception
看這個(gè)代碼片段:
fun main() {
val scope = CoroutineScope(Job())
try {
scope.launch {
throw RuntimeException()
}
} catch (e: Exception) {
println("Caught: $e")
}
Thread.sleep(100)
}
這里的異常catch不住了.
會(huì)直接讓main函數(shù)的主進(jìn)程崩掉.
這是因?yàn)楹推胀ǖ漠惓L幚頇C(jī)制不同, coroutine中未被處理的異常并不是直接拋出, 而是按照job hierarchy向上傳遞給parent.
如果把try放在launch里面還行.
默認(rèn)的異常處理
默認(rèn)情況下, child發(fā)生異常, parent和其他child也會(huì)被取消.
fun main() {
println("start")
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val scope = CoroutineScope(Job() + exceptionHandler)
scope.launch {
println("child 1")
delay(1000)
println("finish child 1")
}.invokeOnCompletion { throwable ->
if (throwable is CancellationException) {
println("Coroutine 1 got cancelled!")
}
}
scope.launch {
println("child 2")
delay(100)
println("child 2 throws exception")
throw RuntimeException()
}
Thread.sleep(2000)
println("end")
}
打印出:
start
child 1
child 2
child 2 throws exception
Coroutine 1 got cancelled!
CoroutineExceptionHandler got java.lang.RuntimeException
end
SupervisorJob
如果有一些情形, 開啟了多個(gè)child job, 但是卻不想因?yàn)槠渲幸粋€(gè)的失敗而取消其他, 怎么辦? 用SupervisorJob
.
比如:
val uiScope = CoroutineScope(SupervisorJob())
如果你用的是scope builder, 那么用supervisorScope
.
用SupervisorJob
改造上面的例子:
fun main() {
println("start")
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val scope = CoroutineScope(SupervisorJob() + exceptionHandler)
scope.launch {
println("child 1")
delay(1000)
println("finish child 1")
}.invokeOnCompletion { throwable ->
if (throwable is CancellationException) {
println("Coroutine 1 got cancelled!")
}
}
scope.launch {
println("child 2")
delay(100)
println("child 2 throws exception")
throw RuntimeException()
}
Thread.sleep(2000)
println("end")
}
輸出:
start
child 1
child 2
child 2 throws exception
CoroutineExceptionHandler got java.lang.RuntimeException
finish child 1
end
盡管coroutine 2拋出了異常, 另一個(gè)coroutine還是做完了自己的工作.
SupervisorJob的特點(diǎn)
SupervisorJob
把取消變成了單向的, 只能從上到下傳遞, 只能parent取消child, 反之不能取消.
這樣既顧及到了由于生命周期的結(jié)束而需要的正常取消, 又避免了由于單個(gè)的child失敗而取消所有.
viewModelScope的context就是用了SupervisorJob() + Dispatchers.Main.immediate
.
除了把取消變?yōu)閱蜗虻? supervisorScope
也會(huì)和coroutineScope
一樣等待所有child執(zhí)行結(jié)束.
在supervisorScope
中直接啟動(dòng)的coroutine是頂級(jí)coroutine.
頂級(jí)coroutine的特性:
- 可以加exception handler.
- 自己處理exception.
比如上面的例子中coroutine child 2可以直接加exception handler.
使用注意事項(xiàng), SupervisorJob
只有兩種寫法:
- 作為
CoroutineScope
的參數(shù)傳入:CoroutineScope(SupervisorJob())
. - 使用
supervisorScope
方法.
把Job作為coroutine builder(比如launch)的參數(shù)傳入是錯(cuò)誤的做法, 不起作用, 因?yàn)橐粋€(gè)新的coroutine總會(huì)assign一個(gè)新的Job.
異常處理的辦法
try-catch
和普通的異常處理一樣, 我們可以用try-catch, 只是注意要在coroutine里面:
fun main() {
val scope = CoroutineScope(Job())
scope.launch {
try {
throw RuntimeException()
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
這樣就能打印出:
Caught: java.lang.RuntimeException
對(duì)于launch, try要包住整塊.
對(duì)于async, try要包住await語(yǔ)句.
scope function: coroutineScope()
coroutineScope
會(huì)把其中未處理的exception拋出來(lái).
相比較于這段代碼中catch不到的exception:
fun main() {
val scope = CoroutineScope(Job())
scope.launch {
try {
launch {
throw RuntimeException()
}
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
沒(méi)走到catch里, 仍然是主進(jìn)程崩潰.
這個(gè)exception是可以catch到的:
fun main() {
val scope = CoroutineScope(Job())
scope.launch {
try {
coroutineScope {
launch {
throw RuntimeException()
}
}
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
打印出:
Caught: java.lang.RuntimeException
因?yàn)檫@里coroutineScope
把異常又重新拋出來(lái)了.
注意這里換成supervisorScope
可是不行的.
CoroutineExceptionHandler
CoroutineExceptionHandler
是異常處理的最后一個(gè)機(jī)制, 此時(shí)coroutine已經(jīng)結(jié)束了, 在這里的處理通常是報(bào)告log, 展示錯(cuò)誤等.
如果不加exception handler那么unhandled exception會(huì)進(jìn)一步往外拋, 如果最后都沒(méi)人處理, 那么可能造成進(jìn)程崩潰.
CoroutineExceptionHandler
需要加在root coroutine上.
這是因?yàn)閏hild coroutines會(huì)把異常處理代理到它們的parent, 后者繼續(xù)代理到自己的parent, 一直到root.
所以對(duì)于非root的coroutine來(lái)說(shuō), 即便指定了CoroutineExceptionHandler
也沒(méi)有用, 因?yàn)楫惓2粫?huì)傳到它.
兩個(gè)例外:
-
async
的異常在Deferred
對(duì)象中,CoroutineExceptionHandler
也沒(méi)有任何作用. - supervision scope下的coroutine不會(huì)向上傳遞exception, 所以
CoroutineExceptionHandler
不用加在root上, 每個(gè)coroutine都可以加, 單獨(dú)處理.
通過(guò)這個(gè)例子可以看出另一個(gè)特性: CoroutineExceptionHandler
只有當(dāng)所有child都結(jié)束之后才會(huì)處理異常信息.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
launch { // the first child
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children terminate")
delay(100)
println("The first child finished its non cancellable block")
}
}
}
launch { // the second child
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
job.join()
}
輸出:
Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException
如果多個(gè)child都拋出異常, 只有第一個(gè)被handler處理, 其他都在exception.suppressed
字段里.
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
} finally {
throw ArithmeticException() // the second exception
}
}
launch {
delay(100)
throw IOException() // the first exception
}
delay(Long.MAX_VALUE)
}
job.join()
}
輸出:
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
單獨(dú)說(shuō)一下async
async比較特殊:
- 作為top coroutine時(shí), 在await的時(shí)候try-catch異常.
- 如果是非top coroutine, async塊里的異常會(huì)被立即拋出.
例子:
fun main() {
val scope = CoroutineScope(SupervisorJob())
val deferred = scope.async {
throw RuntimeException("RuntimeException in async coroutine")
}
scope.launch {
try {
deferred.await()
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
這里由于用了SupervisorJob, 所以async是top coroutine.
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(SupervisorJob() + coroutineExceptionHandler)
topLevelScope.launch {
async {
throw RuntimeException("RuntimeException in async coroutine")
}
}
Thread.sleep(100)
}
當(dāng)它不是top coroutine時(shí), 異常會(huì)被直接拋出.
特殊的CancellationException
CancellationException
是特殊的exception, 會(huì)被異常處理機(jī)制忽略, 即便拋出也不會(huì)向上傳遞, 所以不會(huì)取消它的parent.
但是CancellationException
不能被catch, 如果它不被拋出, 其實(shí)協(xié)程沒(méi)有被成功cancel, 還會(huì)繼續(xù)執(zhí)行.
CancellationException的透明特性:
如果CancellationException
是由內(nèi)部的其他異常引起的, 它會(huì)向上傳遞, 并且把原始的那個(gè)異常傳遞上去.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
val inner = launch { // all this stack of coroutines will get cancelled
launch {
launch {
throw IOException() // the original exception
}
}
}
try {
inner.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
}
}
job.join()
}
輸出:
Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException
這里Handler拿到的是最原始的IOException.
Further Reading
官方文檔:
- Coroutine exceptions handling
Android官方文檔上鏈接的博客和視頻:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-475343.html
- Exceptions in coroutines
- KotlinConf 2019: Coroutines! Gotta catch 'em all! by Florina Muntenescu & Manuel Vivo
其他:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-475343.html
- Kotlin Coroutines and Flow - Use Cases on Android
- Why exception handling with Kotlin Coroutines is so hard and how to successfully master it!
到了這里,關(guān)于[Kotlin Tutorials 22] 協(xié)程中的異常處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!