46

Android 开发中 Kotlin Coroutines 如何优雅地处理异常 - 简书

 4 years ago
source link: https://www.jianshu.com/p/2056d5424001?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0.82019.10.24 02:08:33字数 647阅读 829

一. 尽量少用 GlobalScope

GlobalScope 是 CoroutineScope 的实现类。我们以前使用过的 launch、async 函数都是 CoroutineScope 的扩展函数。

GlobalScope 没有绑定任何 Job 对象,它用于构建最顶层的协程。这些协程的生命周期会跟随着 Application。

在 GlobalScope 中创建的 Coroutines,是有可能会导致应用崩溃的。

fun main() {

    GlobalScope.launch {

        throw RuntimeException("this is an exception")
        "doSomething..."
    }

    Thread.sleep(5000)
}

即使在 main 函数中,增加了 try...catch 试图去捕获异常,下面的代码仍然抛出异常。

fun doSomething(): Deferred<String> = GlobalScope.async {

    throw RuntimeException("this is an exception")
    "doSomething..."
}

fun main() {

    try {
        GlobalScope.launch {
            doSomething().await()
        }
    } catch (e:Exception) {

    }

    Thread.sleep(5000)
}

这是因为,在 doSomething() 内创建了子 Coroutine,子 Coroutine 的异常会导致整个应用的崩溃。

接下来,通过一个自定义 CoroutineScope ,并且它的 CoroutineContext 与 Job 对象相加,Job 对象可以直接管理该协程。但是子 Coroutine 依然可能会抛出异常,从而导致应用的崩溃。

val job: Job = Job()
val scope = CoroutineScope(Dispatchers.Default+job)

fun doSomething(): Deferred<String> = scope.async {

    throw RuntimeException("this is an exception")
    "doSomething..."
}

fun main() {

    try {
        scope.launch {
            doSomething().await()
        }
    } catch (e:Exception) {

    }

    Thread.sleep(5000)
}

二. SupervisorJob、CoroutineExceptionHandler 的使用

对于 GlobalScope 创建的 Coroutines,前面已经介绍过可能会导致 Crash。

        text1.setOnClickListener {

            GlobalScope.launch(UI) {

                Toast.makeText(mContext,"cannot handle the exception", Toast.LENGTH_SHORT).show()

                throw Exception("this is an exception")
            }
        }

如果能够创建一个 CoroutineScope,由该 CoroutineScope 创建的 Coroutines 即使抛出异常,依然能够捕获,那将是多么的理想。

        text2.setOnClickListener {

            uiScope().launch {

                Toast.makeText(mContext,"handle the exception", Toast.LENGTH_SHORT).show()

                throw Exception("this is an exception")
            }
        }

如果还能够对异常做一些处理,那将是再好不过的了。

        text3.setOnClickListener {

            val errorHandle = object : CoroutineErrorListener {
                override fun onError(throwable: Throwable) {

                    Log.e("errorHandle",throwable.localizedMessage)
                }
            }

            uiScope(errorHandle).launch {

                Toast.makeText(mContext,"handle the exception", Toast.LENGTH_SHORT).show()

                throw Exception("this is an exception")
            }
        }

上面使用的 uiScope 是调用的 SafeCoroutineScope 来创建的 CoroutineScope。

SafeCoroutineScope 的 CoroutineContext 使用了 SupervisorJob 和 CoroutineExceptionHandler。

SupervisorJob 里面的子 Job 不相互影响,一个子 Job 的失败,不会不影响其他子 Job 的执行。

CoroutineExceptionHandler 和使用 Thread.uncaughtExceptionHandler 很相似。 CoroutineExceptionHandler 被用来将通用的 catch 代码块用于在协程中自定义日志记录或异常处理。

我们来看一下它们的封装:

val UI: CoroutineDispatcher      = Dispatchers.Main

fun uiScope(errorHandler: CoroutineErrorListener?=null) = SafeCoroutineScope(UI,errorHandler)

class SafeCoroutineScope(context: CoroutineContext, errorHandler: CoroutineErrorListener?=null) : CoroutineScope, Closeable {

    override val coroutineContext: CoroutineContext = SupervisorJob() + context + UncaughtCoroutineExceptionHandler(errorHandler)

    override fun close() {
        coroutineContext.cancelChildren()
    }
}

class UncaughtCoroutineExceptionHandler(val errorHandler: CoroutineErrorListener?=null)  :
        CoroutineExceptionHandler, AbstractCoroutineContextElement(CoroutineExceptionHandler.Key) {

    override fun handleException(context: CoroutineContext, throwable: Throwable) {
        throwable.printStackTrace()

        errorHandler?.let {
            it.onError(throwable)
        }
    }
}

所以,点击 text2、text3 按钮时,不会导致 App Crash。

在点击 text4 时,即使子 Coroutine 抛出异常,也不会导致 App Crash

        text4.setOnClickListener {

            uiScope().launch {

                try {
                    uiScope().async {

                        throw RuntimeException("this is an exception")
                        "doSomething..."
                    }.await()
                } catch (e: Exception) {

                }
            }

            Toast.makeText(mContext,"handle the exception", Toast.LENGTH_SHORT).show()
        }

三. 在 View 中创建 autoDisposeScope

在 Android View 中创建的 Coroutines,需要跟 View 的生命周期绑定。

下面定义的 View 的扩展属性 autoDisposeScope,也是借助 SafeCoroutineScope。

// 在 Android View 中创建 autoDisposeScope,支持主线程运行、异常处理、Job 能够在 View 的生命周期内自动 Disposable
val View.autoDisposeScope: CoroutineScope
    get() {
        return SafeCoroutineScope(UI + ViewAutoDisposeInterceptorImpl(this))
    }

有了 autoDisposeScope 这个 CoroutineScope,就可以在 View 中放心地使用 Coroutines。

        text2.setOnClickListener {

            text2.autoDisposeScope.launch {

                doSomeWork()
            }
        }

去年在《AAC 的 Lifecycle 结合 Kotlin Coroutines 进行使用》 一文中,曾经介绍过我封装的库:https://github.com/fengzhizi715/Lifecycle-Coroutines-Extension,本文是对该库的一次升级,也是对近期使用 Kotlin Coroutines 的经验总结。

https://github.com/fengzhizi715/Lifecycle-Coroutines-Extension,不仅是对 Lifecycle 的封装,也可以说是对 Coroutines 使用的封装。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK