Domine completamente o tratamento de exceções de rotina Kotlin

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 CoroutineExceptionHandlerpara definir o manipulador de exceções da corrotina. Quando ocorre uma exceção em uma corrotina, ela é passada para handleruma 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 JobSupportdentro da classe.Se pudermos esclarecer o processo a seguir, será útil entender o processo de tratamento de exceções da corrotina.

fba66978b89241173297877dfbd87819.jpeg

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

jobcoroutineContextO 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 handleJobExceptiono método. Aprendi antes que   launch {}iniciar uma corrotina na verdade cria um StandaloneCoroutineobjeto,  StandaloneCoroutinemas JobSupport uma classe, o que significa que o método StandaloneCoroutineda 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.handleJobExceptionStandaloneCoroutinehandleJobExceptiontrue

74cb551a7a7d631e39c316c8172005ce.jpeg

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:handleJobExceptionCoroutineExceptionHandlerCoroutineExceptionHandler

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, cancelParento método finalmente chama o método jobe retorna. Finalmente, o método co-rotina será executado para tratamento de exceções. O oficial também fornece uma classe de implementação semelhantechildCancelledchildCancelledfalsehandleJobExceptionSupervisorJobImpl

public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)


private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

Parece SupervisorJobfamiliar? Acontece que é apenas uma função. A implementação real é que efeitos semelhantes podem ser alcançados SupervisorJobImplusandoSupervisorJob

private fun test08() {
   lifecycleScope.launch {
        launch {
            val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
                Log.d(TAG, "test08: ${throwable.message}")
            }
            launch(SupervisorJob() + coroutineExceptionHandler) {
                throw Exception("test08")
            }
        }
    }
}

Se especificarmos launchone 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 catchna 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 UndispatchedCoroutinecorrotina será aberta. Esta classe herda da ScopeCoroutineclasse. É 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. CPSSe 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, CPSa 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:

  1. Depois que o método de suspensão é convertido por cps, o tipo de valor de retorno é modificado para o Objecttipo

  2. O 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)

  3. Após a conclusão da execução do método de suspensão, a execução chega   case 1:ao código e o resultvalor do resultado é verificado. Se resultfor Exceptiondo tipo, a exceção é lançada novamente.

raw.githubusercontent.com/eric-lian/b…

Resumir

Se try ... catcho 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()
    }
}

abfcb0146b771c00dcb38c11afa16623.jpeg

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 falsequanto 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óprio handleJobExceptionmé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 e async 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

a27679d85a1e4e920305dc8ac90a3e33.jpeg

b8d2917da7ec7311f20923ba72d0489c.jpeg

Acho que você gosta

Origin blog.csdn.net/c6E5UlI1N/article/details/132550679
Recomendado
Clasificación