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 CoroutineExceptionHandler
para definir el controlador de excepciones de la corrutina. Cuando ocurre una excepción en una rutina, se pasa a handler
una 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á JobSupport
dentro de la clase. Si podemos aclarar el siguiente proceso, nos será útil comprender el proceso de manejo de excepciones de la rutina.
// 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
El 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 handleJobException
al método. Aprendí antes que launch {}
iniciar una rutina en realidad crea un StandaloneCoroutine
objeto, StandaloneCoroutine
pero JobSupport
una clase, lo que significa que el método StandaloneCoroutine
de 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.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
}
}
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: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")
}
Es decir, cancelParent
el método finalmente llama al método job
de 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.childCancelled
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
}
¿ Te SupervisorJob
suena familiar? Resulta que es solo una función. La implementación real es que se pueden lograr efectos similares SupervisorJobImpl
usandoSupervisorJob
private fun test08() {
lifecycleScope.launch {
launch {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.d(TAG, "test08: ${throwable.message}")
}
launch(SupervisorJob() + coroutineExceptionHandler) {
throw Exception("test08")
}
}
}
}
Si launch
especificamos 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 catch
de 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. UndispatchedCoroutine
Se abrirá una nueva corrutina. Esta clase hereda de ScopeCoroutine
la 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. CPS
Si 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, CPS
la 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:
Después de que cps convierte el método de suspensión, el tipo de valor de retorno se modifica para
Object
escribirEl 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).
Una vez completada la ejecución del método de suspensión, la ejecución llega
case 1:
al código yresult
se verifica el valor del resultado, siresult
esException
de tipo, se vuelve a lanzar la excepción.
raw.githubusercontent.com/eric-lian/b…
Resumir
Si try ... catch
el 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()
}
}
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 false
como 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
CoroutineExceptionHandler
es si la rutina principal puede manejar la excepción lanzada por la rutina secundaria y si el propiohandleJobException
mé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 yasync
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.