Dominar a fondo el manejo de excepciones de rutina de Kotlin

Autor: Lstone7364 Enlace: https://juejin.cn/post/7270478168758829111

Al aprender el manejo de excepciones de rutina de Kotlin, existen dos soluciones recomendadas por los documentos oficiales:

1. Utilizado en el cuerpo de rutina.try { .. } catch(e: Exception) {...}

launch {
    try {
        // 协程代码
    } catch (e: Exception) {
    // 异常处理
    }
}

2. Úselo CoroutineExceptionHandlerpara definir el controlador de excepciones de la corrutina. Cuando ocurre una excepción en una rutina, se pasa a handleruna función para su manejo.

val handler = CoroutineExceptionHandler { _, exception ->
    // 异常处理
}


val scope = CoroutineScope(Dispatchers.IO + handler)
scope.launch {
    // 协程代码
}

Prueba de manejo de excepciones de rutina

Entonces, para los siguientes casos de prueba para corrutinas, ¿cuáles se detectarán con éxito con excepciones y cuáles fallarán?

// 不会出现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álisis del código fuente del escenario.

Después del ejemplo anterior, puede ver que el manejo de excepciones de las corrutinas no es tan simple, pero si entendemos el código fuente, será mucho más claro. Al observar el código fuente, cuando ocurre una excepción en la rutina, eventualmente irá JobSupportdentro de la clase. Si podemos aclarar el siguiente proceso, nos será útil comprender el proceso de manejo de excepciones de la rutina.

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

jobcoroutineContextEl objeto que establece los parámetros para el lanzamiento (coroutineContext) al crear una corrutina coroutineContext[Job], generalmente el objeto de corrutina principal.

Si cancelParent devuelve falso, intente llamar handleJobExceptional método. Aprendí antes que   launch {}iniciar una rutina en realidad crea un StandaloneCoroutineobjeto,  StandaloneCoroutinepero JobSupport una clase, lo que significa que el método StandaloneCoroutinede la clase eventualmente se ejecutará. Al mirar el código fuente, descubrí que se implementó y el valor de retorno es fijo , lo que indica que la excepción ha sido manejada.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
    }
}

Vea la implementación específica de handleCoroutineException. Si el contexto [CoroutineExceptionHandler] no está vacío, se llamará a la configuración de rutina actual CoroutineExceptionHandler para la entrega y el procesamiento de excepciones.

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

En los materiales de aprendizaje anteriores, aprendimos que CoroutineExceptionHandler solo será efectivo si se establece en la rutina raíz, ¿es este realmente el caso? Al observar el código fuente, descubrimos que siempre que se garantice que cancelParent(finalException) el valor de retorno se establezca llamando a la corrutina dentro de la implementación del método false  de corrutina , también se pueden usar subcorrutinas . Podemos personalizar el trabajo para manejar excepciones. El código es el siguiente: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")
}

Es decir, cancelParentel método finalmente llama al método jobde y regresa. Finalmente, el método de la corrutina se ejecutará para el manejo de excepciones. El funcionario también proporciona una clase de implementación similar.childCancelledchildCancelledfalsehandleJobExceptionSupervisorJobImpl

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


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

¿ Te SupervisorJobsuena familiar? Resulta que es solo una función. La implementación real es que se pueden lograr efectos similares SupervisorJobImplusandoSupervisorJob

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

Si launchespecificamos uno SupervisorJob(), podemos detectar con éxito la excepción, pero ha violado el principio de concurrencia estructurada de rutina. Debido a que la especificación SupervisorJob()ha abandonado el alcance de la rutina de contexto actual, es decir, cuando se cierra la página actual, la rutina no se cancelará cuando se cierre la página. El enfoque correcto es el siguiente:

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 suspensión

Manejo especial de excepciones mediante métodos de suspensión.

El método suspend manejará la excepción una vez y la volverá a lanzar dentro catchde la corrutina .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()
}

Tomemos withContext()esto como ejemplo. UndispatchedCoroutineSe abrirá una nueva corrutina. Esta clase hereda de ScopeCoroutinela clase. Es muy importante que el siguiente código esté incluido en la clase.

final override val isScopedCoroutine: Boolean get() = true

Esta variable se utiliza en los siguientes lugares

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 , 即表示当前协程无需处理异常 , 既异常会被挂起方法拦截。
El método de suspensión vuelve a lanzar excepciones
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)
    }
}

En el código fuente, puede ver que el método de suspensión intercepta la excepción y devuelve la información de la excepción a la rutina principal nuevamente. CPSSi está familiarizado con la conversión de rutinas , sabrá que la rutina de Kotlin se implementa en función del mecanismo de la máquina de estado, es decir, CPSla conversión se realiza en tiempo de compilación y esta conversión es imperceptible. El siguiente código:

private suspend fun testSuspend() {
    try {
        sum()
    } catch (e: Exception){ }
}


private suspend fun sum(): Int {
    return 0
}

Dado que el método de suspensión capturará la excepción y la devolverá a la rutina principal, el código de ejecución real después de la compilación se entiende como el siguiente código

private suspend fun testSuspend() {
    try {
        val result = sum()
        if (result is Exception) {
             throw e
        }
        next step
    } catch (e: Exception){ }
}

Lo mismo ocurre con el proceso del código fuente real después de la descompilación:

  1. Después de que cps convierte el método de suspensión, el tipo de valor de retorno se modifica para Objectescribir

  2. El compilador agrega un parámetro de tipo Continuación al método de suspensión, que se utiliza para devolver los resultados del método de suspensión y cambiar el estado de la máquina de estados (evocando el punto de suspensión para continuar ejecutando el siguiente paso).

  3. Una vez completada la ejecución del método de suspensión, la ejecución llega   case 1:al código y resultse verifica el valor del resultado, si resultes Exceptionde tipo, se vuelve a lanzar la excepción.

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

Resumir

Si try ... catchel manejo de excepciones se realiza en un método suspendido, siempre que el tipo de excepción sea preciso, la excepción se puede detectar con éxito.

¿Por qué no se pueden detectar excepciones?
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

Después de que ocurre una excepción, se pasará a la rutina principal o se manejará sola dentro del contexto de la rutina actual. Lo mismo ocurre con la rutina principal. Diferentes corrutinas manejan excepciones de diferentes maneras. Por ejemplo, cuando una corrutina no maneja la corrutina secundaria, tanto el método cancelParent()regresa falsecomo la corrutina secundaria launch inicia la corrutina. StandCoroutine.handleException()El método de procesamiento cerrará directamente la aplicación. En la rutina iniciada por async DeferredCoroutine, la excepción actual se almacena en caché y DeferredCoroutine.handleException()no se procesará inmediatamente. Se async.await() volverá a generar cuando se realice la llamada para obtener el valor.

Reponer

Recientemente, encontré problemas de excepción de rutina en algunos escenarios. Me gustaría agregarlos aquí y resumirlos de la siguiente manera:

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
  • Por lo tanto, el punto clave sobre si una rutina se puede personalizar CoroutineExceptionHandleres si la rutina principal puede manejar la excepción lanzada por la rutina secundaria y si el propio handleJobExceptionmétodo de la rutina puede manejar la excepción.

  • Si desea juzgar si el try...catch actual puede tener efecto, solo necesita verificar si el lugar actual donde se puede lanzar la excepción es un método de suspensión. Si es un método de suspensión, debe estar bien catch.

  • Al leer algunos artículos, se dice que launch las excepciones se transmiten vertical y async horizontalmente. Creo que esta afirmación no es apropiada. Desde la perspectiva del código fuente, las excepciones siempre se transmiten verticalmente, pero pueden ser interceptadas y procesadas en el proceso.

Sígueme para obtener más conocimientos o contribuir con artículos.

a27679d85a1e4e920305dc8ac90a3e33.jpeg

b8d2917da7ec7311f20923ba72d0489c.jpeg

Supongo que te gusta

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