Autor: Lstone7364 Link: https://juejin.cn/post/7270478168758829111
Ao aprender o tratamento de exceções de rotina Kotlin, existem duas soluções recomendadas por documentos oficiais:
1. Usado no corpo da rotinatry { .. } catch(e: Exception) {...}
launch {
try {
// 协程代码
} catch (e: Exception) {
// 异常处理
}
}
2. Use CoroutineExceptionHandler
para definir o manipulador de exceções da corrotina. Quando ocorre uma exceção em uma corrotina, ela é passada para handler
uma função para tratamento.
val handler = CoroutineExceptionHandler { _, exception ->
// 异常处理
}
val scope = CoroutineScope(Dispatchers.IO + handler)
scope.launch {
// 协程代码
}
Teste de tratamento de exceção de corrotina
Portanto, para os seguintes casos de teste de corrotinas, quais serão capturados com êxito com exceções e quais travarão?
// 不会出现crash
private fun test01() {
val async = lifecycleScope.async {
throw RuntimeException()
}
}
// 不会出现crash ,异常会被成功catch并打印
private fun test02() {
val async = lifecycleScope.async {
throw RuntimeException()
}
lifecycleScope.launch {
try {
async.await()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// 不出现crash
private fun test03() {
lifecycleScope.async {
async {
throw RuntimeException()
}
}
}
// 出现crash
private fun test04() {
lifecycleScope.launch {
async {
throw RuntimeException()
}
}
}
// 不会出现crash
private fun test05() {
lifecycleScope.async {
launch {
throw RuntimeException()
}
}
}
// 出现crash
private fun test06() {
lifecycleScope.launch {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "test02: ${throwable.message}")
}
launch(coroutineExceptionHandler) {
throw Exception("test02")
}
}
}
// 不会出现 crash , 异常被 coroutineExceptionHandler 处理
private fun test07() {
lifecycleScope.launch {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "test02: ${throwable.message}")
}
launch(SupervisorJob() + coroutineExceptionHandler) {
throw Exception("test02")
}
}
}
// 不会crash , 但是 coroutineExceptionHandler 未输出异常信息
private fun test08() {
lifecycleScope.launch {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "test02: ${throwable.message}")
}
async(SupervisorJob() + coroutineExceptionHandler) {
throw Exception("test02")
}
}
}
Análise do código-fonte do cenário
Após o exemplo acima, você pode ver que o tratamento de exceções de corrotinas não é tão simples, mas se entendermos o código-fonte, ficará muito mais claro. Olhando para o código-fonte, quando ocorre uma exceção na corrotina, ela eventualmente irá para JobSupport
dentro da classe.Se pudermos esclarecer o processo a seguir, será útil entender o processo de tratamento de exceções da corrotina.
// JobSupport 类
private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
...
// Now handle the final exception
if (finalException != null) {
val handled = cancelParent(finalException) || handleJobException(finalException)
if (handled) (finalState as CompletedExceptionally).makeHandled()
}
...
}
private fun cancelParent(cause: Throwable): Boolean {
val isCancellation = cause is CancellationException
val parent = parentHandle
// No parent -- ignore CE, report other exceptions.
if (parent === null || parent === NonDisposableHandle) {
return isCancellation
}
// Notify parent but don't forget to check cancellation
return parent.childCancelled(cause) || isCancellation
}
protected open fun handleJobException(exception: Throwable): Boolean = false
isCancellation = false
,cancelParent
方法内的 parent.childCancelled(cause)
方法返回为true 可以理解为父协程可以接收或者处理子协程抛出的异常,false
表示父协程不处理子协程相关异常 。 parent
的一般为
ChildHandleNode
,ChildHandleNode.cancelParent(finalException)
的最终调用实现为
internal class ChildHandleNode(
@JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
override val parent: Job get() = job
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}
job
coroutineContext
O objeto que define parâmetros para launch(coroutineContext) ao criar uma coroutine coroutineContext[Job]
, geralmente o objeto pai da coroutine.
Se cancelParent retornar falso, tente chamar handleJobException
o método. Aprendi antes que launch {}
iniciar uma corrotina na verdade cria um StandaloneCoroutine
objeto, StandaloneCoroutine
mas JobSupport
uma classe, o que significa que o método StandaloneCoroutine
da classe será eventualmente executado.Olhando o código-fonte, descobri que ele foi implementado e o valor de retorno é fixo , indicando que a exceção foi tratada.handleJobException
StandaloneCoroutine
handleJobException
true
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
override fun handleJobException(exception: Throwable): Boolean {
handleCoroutineException(context, exception)
return true
}
}
Veja a implementação específica de handleCoroutineException. Se context[CoroutineExceptionHandler] não estiver vazio, a configuração de corrotina atual CoroutineExceptionHandler será chamada para entrega e processamento de exceção.
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
// Invoke an exception handler from the context if present
try {
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
// If a handler is not present in the context or an exception was thrown, fallback to the global handler
handleCoroutineExceptionImpl(context, exception)
}
Nos materiais de aprendizagem anteriores, aprendemos que CoroutineExceptionHandler só será eficaz se estiver definido como a corrotina raiz. Esse é realmente o caso? Observando o código-fonte, descobrimos que, desde que seja garantido que cancelParent(finalException)
o valor de retorno seja definido chamando a corrotina dentro da implementação do método false
da corrotina , sub-rotinas também podem ser usadas . Podemos personalizar o trabalho para lidar com exceções. O pseudo o código é o seguinte:handleJobException
CoroutineExceptionHandler
CoroutineExceptionHandler
val job = object: JobSupport {
override fun childCancelled(cause: Throwable): Boolean {
return false
}
}
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d(TAG, "onCreate: ${throwable.message}")
}
lifecycleScope.launch(job + exceptionHandler) {
throw Exception("test")
}
Ou seja, cancelParent
o método finalmente chama o método job
e retorna. Finalmente, o método co-rotina será executado para tratamento de exceções. O oficial também fornece uma classe de implementação semelhantechildCancelled
childCancelled
false
handleJobException
SupervisorJobImpl
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
}
Parece SupervisorJob
familiar? Acontece que é apenas uma função. A implementação real é que efeitos semelhantes podem ser alcançados SupervisorJobImpl
usandoSupervisorJob
private fun test08() {
lifecycleScope.launch {
launch {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "test08: ${throwable.message}")
}
launch(SupervisorJob() + coroutineExceptionHandler) {
throw Exception("test08")
}
}
}
}
Se especificarmos launch
one SupervisorJob()
, podemos capturar a exceção com sucesso, mas ela violou o princípio da simultaneidade estruturada de co-rotina. Porque a especificação SupervisorJob()
saiu do escopo da corrotina do contexto atual, ou seja, quando a página atual for fechada, a corrotina não será cancelada quando a página for fechada. A abordagem correta é a seguinte:
private fun test09() {
lifecycleScope.launch {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "test09: ${throwable.message}")
}
async(SupervisorJob(coroutineContext[Job]) + coroutineExceptionHandler) {
throw Exception("test09")
}
}
}
método de suspensão
Tratamento especial de exceções suspendendo métodos
O método suspend tratará a exceção uma vez e a lançará novamente catch
na corrotina .afterResume()
private suspend fun testTrySuspend() {
try {
// 只要时
trySuspend()
} catch (e: Exception) { }
}
private suspend fun trySuspend() {
// 方式1 抛出异常
// throw RuntimeException()
// 方式2 切换协程
//withContext(Dispatchers.IO) {
// throw RuntimeException()
//}
// 方式3 调用其他挂起方法
// invokeOtherSuspend()
}
Vamos tomar withContext()
isso como exemplo. Uma nova UndispatchedCoroutine
corrotina será aberta. Esta classe herda da ScopeCoroutine
classe. É muito importante que o código a seguir esteja incluído na classe.
final override val isScopedCoroutine: Boolean get() = true
Esta variável é usada nos seguintes locais
private fun cancelParent(cause: Throwable): Boolean {
// 注释也做了说明
// Is scoped coroutine -- don't propagate, will be rethrown
if (isScopedCoroutine) return true
/* CancellationException is considered "normal" and parent usually is not cancelled when child produces it.
* This allow parent to cancel its children (normally) without being cancelled itself, unless
* child crashes and produce some other exception during its completion.
*/
val isCancellation = cause is CancellationException
val parent = parentHandle
// No parent -- ignore CE, report other exceptions.
if (parent === null || parent === NonDisposableHandle) {
return isCancellation
}
// Notify parent but don't forget to check cancellation
return parent.childCancelled(cause) || isCancellation
}
看到这里是不是很熟悉, 当异常发生的时候,就会调用该方法, 可以看到当isScopedCoroutine == true时,直接返回了true , 即表示当前协程无需处理异常 , 既异常会被挂起方法拦截。
O método Suspend relança exceções
public final override fun resumeWith(result: Result<T>) {
val state = makeCompletingOnce(result.toState())
if (state === COMPLETING_WAITING_CHILDREN) return
afterResume(state)
}
override fun afterResume(state: Any?) {
threadStateToRecover.get()?.let { (ctx, value) ->
restoreThreadContext(ctx, value)
threadStateToRecover.set(null)
}
// resume undispatched -- update context but stay on the same dispatcher
val result = recoverResult(state, uCont)
withContinuationContext(uCont, null) {
// 异常重抛
uCont.resumeWith(result)
}
}
No código-fonte, você pode ver que o método de suspensão intercepta a exceção e retorna as informações da exceção para a rotina pai novamente. CPS
Se você está familiarizado com a conversão de corrotinas , saberá que a corrotina Kotlin é implementada com base no mecanismo da máquina de estados, ou seja, CPS
a conversão é realizada em tempo de compilação e essa conversão é imperceptível. O seguinte código:
private suspend fun testSuspend() {
try {
sum()
} catch (e: Exception){ }
}
private suspend fun sum(): Int {
return 0
}
Como o método de suspensão irá capturar a exceção e devolvê-la à corrotina pai, o código real em execução após a compilação é entendido como o seguinte código
private suspend fun testSuspend() {
try {
val result = sum()
if (result is Exception) {
throw e
}
next step
} catch (e: Exception){ }
}
O mesmo se aplica ao processo real do código-fonte após a descompilação:
Depois que o método de suspensão é convertido por cps, o tipo de valor de retorno é modificado para o
Object
tipoO compilador adiciona um parâmetro do tipo Continuation ao método de suspensão, que é usado para retornar o resultado do método de suspensão e mudar o estado da máquina de estados (evocando o ponto de suspensão para continuar executando a próxima etapa)
Após a conclusão da execução do método de suspensão, a execução chega
case 1:
ao código e oresult
valor do resultado é verificado. Seresult
forException
do tipo, a exceção é lançada novamente.
raw.githubusercontent.com/eric-lian/b…
Resumir
Se try ... catch
o tratamento de exceções for executado em um método suspenso, desde que o tipo de exceção seja preciso, a exceção poderá ser capturada com êxito.
Por que não é possível capturar exceções?
private fun test10() {
try {
lifecycleScope.launch {
// 1. 异常抛出
// 2. JobSupport 执行 parent.childCancel(e) || handleException()
// 3. 父job不处理, 执行 StandaloneCoroutine.handleException()
throw RuntimeException()
}
} catch (e: Exception) {}
}
private fun test11() {
try {
lifecycleScope.async {
// 1. 异常抛出
// 2. JobSupport 执行 parent.childCancel(e) || handleException()
// 3. 父job不处理, 执行 DeferredCoroutine.handleException() . 默认未处理
// 异常 , 忽略异常。
throw RuntimeException()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
Depois de ocorrer uma exceção,ela será passada para a corrotina pai ou tratada por si mesma dentro do contexto da corrotina atual. O mesmo se aplica à corrotina pai. Diferentes corrotinas tratam exceções de maneiras diferentes. Por exemplo, quando uma corrotina não trata a corrotina filha, tanto o método cancelParent()
retorna false
quanto a corrotina filha launch
inicia a corrotina. StandCoroutine.handleException()
O método de processamento matará diretamente o aplicativo. Na corrotina iniciada por async DeferredCoroutine
, a exceção atual é armazenada em cache e DeferredCoroutine.handleException()
não será processada imediatamente. async.await()
Ela será lançada novamente quando o valor for chamado.
Reabastecimento
Recentemente, encontrei problemas de exceção de rotina em alguns cenários. Gostaria de adicionar um resumo e resumi-lo da seguinte forma:
private fun testCoroutineMultiThrows() {
lifecycleScope.launch {
try {
test()
} catch (e: Exception) {
println("==== test exception")
}
}
}
private suspend fun test() {
suspendCancellableCoroutine<Unit> {
// 连续两次调用 resumeWithException 也只会捕捉到一次异常
//,这个和 throws 是一个道理,抛出一个异常后,后续代码不再执行
// 如果调用方不进行 catch 则会crash
//it.resumeWithException(Exception("test exception"))
//it.resumeWithException(Exception("test exception"))
// 抛出 kotlinx.coroutines.CancellationException
// 如果添加了try catch 则可以catch到该异常,
// 不添加try catch 程序也不会崩溃,协程框架会忽略该类型异常处理
//it.cancel(CancellationException("test exception"))
// 同上
//throw CancellationException("test exception")
// 不添加try catch 会导致应用crash
//throw Exception("test exception")
}
Resumir
Portanto, o ponto principal para saber se uma corrotina pode ser personalizada
CoroutineExceptionHandler
é se a corrotina pai pode tratar a exceção lançada pela corrotina filha e se o própriohandleJobException
método da corrotina pode tratar a exceção.Se você quiser avaliar se o try...catch atual pode entrar em vigor, você só precisa verificar se o local atual onde a exceção pode ser lançada é um método suspenso. Se for um método suspenso, deve estar OK
catch
.Ao ler alguns artigos, diz-se que
launch
as exceções são transmitidas verticalmente easync
horizontalmente. Acho que esta afirmação não é apropriada. Do ponto de vista do código-fonte, as exceções são sempre transmitidas verticalmente, mas podem ser interceptadas e processadas no processo.
Siga-me para obter mais conhecimento ou contribuir com artigos