美文网首页kotlinKotlin
全网最详细的Kotlin协程-异常篇讲解与踩坑

全网最详细的Kotlin协程-异常篇讲解与踩坑

作者: imkobedroid | 来源:发表于2022-03-23 17:49 被阅读0次

    前言

    协程的使用中对异常的处理是非常抽象的一个过程,google了很多文档,在官方文档中对异常的处理并没有讲的很详细,编写过程中踩的坑似乎也没有官方文档的说明与解释,网上也有很对对异常的处理文献,但是看过之后发现都是零零散散,而且很多案例都是没经过代码推敲的,甚至有些文献里面的理解是错误的,所以奔着开发的理念仔细研究了一下协程的异常处理,以便更多的朋友看到这篇文章能带来更好的理解,也对封装框架设计有很大的帮助,以下案例均可以拷贝到编译器进行自行验证,如有理解不对的地方欢迎私信我进行交流学习并改正

    概念

    Try Catch能捕获所有的异常吗?

    答案是不能,简单的举例说明:

    • 情况一:如果程序发生了异常并没有进行抛出,这个时候会捕获不到异常
    • 情况二:在java中如果程序抛出的是错误,而不是异常这种情况视捕获的代码形态决定能否捕获到异常
    • 情况三:比如动态链接库的加载错误,以及部分系统错误引起的异常不一定能捕获到

    协程异常了怎么办?

    当一个协程发生了异常,它将把异常传播给它的父协程,父协程会做以下几件事:

    1. 取消其他子协程
    2. 取消自己
    3. 将异常传播给自己的父协程

    所以要理解协程异常的处理需要弄清楚下面几个关键点:

    • try-catch捕获异常
    • CoroutineExceptionHandler
    • supervisorScope 和SupervisorJob

    程序示例

    看下面的代码

      fun test() {
            try {
                Thread() {
                    throw NullPointerException()
                }.start()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    

    结果是:运行崩溃

    这里如果有朋友觉得很不可思议的话可以进行自我测试,为什么try-catch中开启代码还是会崩溃呢?

    ==答案是try-catch 只能捕捉当前线程的堆栈信息。对于非当前线程无法实现捕捉==

    既然这样下面代码应该会被捕捉到:

    fun test() = runBlocking(Dispatchers.IO) {
            try {
                launch {
                    throw NullPointerException()
                }
            } catch (e: Exception) {
                e.printStackTrace()
                Log.d("wangxuyang", "" + e.message)
            }
        }
    

    结果是:运行崩溃

    what f***?,这个协程是在当前线程开启的,并进行了try-catch为什么还是会崩溃呢?

    这里直接告诉结论是:==launch启动的根协程,是不会传播异常的==


    什么叫传播异常?

    传播异常,是指能够将异常主动往外抛到启动顶层协程所在的线程。因为launch启动的协程,是不会将异常抛到线程,所以try-catch无法捕捉,为了让这种异常能够捕捉到。协程引入了CoroutineExceptionHandler

    启动协程还有一种方式是async,那这种会不会向线程抛出异常呢?代码运行如下:

     private val job: Job = Job()
        private val scope = CoroutineScope(Dispatchers.Default + job)
    
        private fun doWork(): Deferred<String> = scope.async { throw NullPointerException("自定义空指针异常") }
    
    
        private fun loadData() = scope.launch {
            try {
                doWork().await()
            } catch (e: Exception) {
                Log.d("try catch捕获的异常:", e.toString())
            }
        }
    
    

    结果是:运行不会崩溃

    代码中try-catch住的代码是:

    doWork().await()
    

    结论:==虽然向外抛出了异常,但是是在调用await()方法后抛出的,并且当async作为根协程时,被封装到deferred对象中的异常才会在调用await时抛出,并且这个异常是可以被try-catch捕获住的==

    上面说到根协程并且这个根协程是调用了await()抛出异常,其实这里是一个大坑,笔者在测试过程中感到也很神奇,接下来看这段代码:

      private val job0: Job = Job()
      private val scope0 = CoroutineScope(Dispatchers.Default + job0)
      private fun loadData0() = scope0.launch {
            val asy = async {
                Log.d("async 异常:", "开始准备抛出异常")
                delay(1000)
                throw NullPointerException("自定义空指针异常")
            }
            try {
                asy.await()
            } catch (e: Exception) {
                Log.d("async 异常: 捕获的异常-", e.toString())
            }
            Log.d("async 异常:", "继续执行后续代码")
        }
    

    运行结果是:程序崩溃

    2022-03-22 19:51:02.074 25864-25903/com.example.coroutinestest D/async 异常:: 开始准备抛出异常
    2022-03-22 19:51:03.085 25864-25905/com.example.coroutinestest D/async 异常: 捕获的异常-: java.lang.NullPointerException: 自定义空指针异常
    2022-03-22 19:51:03.085 25864-25905/com.example.coroutinestest D/async 异常:: 继续执行后续代码
    

    乍一看,跟上面代码的逻辑走势一样,也是调用了await方法,也是try-catch了这个方法 ,打的日志也是捕获到了,是正常的流程啊
    但是我告诉大家这里并不是调用await方法后才抛出的异常,只是崩溃后这个异常被捕获到了而已,是不是大家要觉得我很菜?可以这样来印证这个猜想,讲await方法屏蔽掉,再运行这个方法:

      try {
              // asy.await()
          } catch (e: Exception) {
                Log.d("async 异常: 捕获的异常-", e.toString())
          }
            Log.d("async 异常:", "继续执行后续代码")
    

    结果是:程序崩溃,日志如下

     //2022-03-22 19:55:05.460 26378-26415/com.example.coroutinestest D/async 异常:: 继续执行后续代码
     //2022-03-22 19:55:05.461 26378-26415/com.example.coroutinestest D/async 异常:: 开始准备抛出异常
    

    这里是不是印证了前面的猜想,崩溃原因其实不是在调用await方法之后引起的崩溃,是代码执行到 throw NullPointerException("自定义空指针异常")就抛出异常了,所以前面的结论是成立的

    结论是:==async开启一个根协程或者子协程,异常都会被抛出给线程,并且可以被try-catch捕获到。async开启一个根协程,在调用await方法时候会抛出异常,这个异常可以用try-catch捕获不引起崩溃,如果这个协程不是根协程,那么是代码执行到 throw 异常的时候就抛出了异常与是否调用await方法无关这个异常可以用try-catch捕获但是会引起崩溃,可以用CoroutineExceptionHandler进行捕获解决崩溃问题==

    CoroutineExceptionHandler的应用

    上面印证了程序的崩溃与异常的抛出,但是这个异常怎么处理呢?这里就用到了官方提供的CoroutineExceptionHandler了

    /**
     * Creates a [CoroutineExceptionHandler] instance.
     * @param handler a function which handles exception thrown by a coroutine
     */
    

    ==CoroutineExceptionHandler的官方解释是:处理协程抛出的异常的函数,官方又一个隐藏点没说就是这个CoroutineExceptionHandler只能处理当前域内开启的子协程或者当前协程抛出的异常==

    所以解决上诉不是根协程引起的崩溃问题可以采用这样的方式:

     private val coroutineExceptionHandler = CoroutineExceptionHandler { _, _ ->
            Log.d("async 异常:", "异常被内部CoroutineExceptionHandler处理掉了")
        }
    
        private fun loadData0() = scope0.launch(coroutineExceptionHandler) {
            val asy = async {
                Log.d("async 异常:", "开始准备抛出异常")
                delay(1000)
                throw NullPointerException("自定义空指针异常")
            }
            try {
                asy.await()
            } catch (e: Exception) {
                Log.d("async 异常: 捕获的异常-", e.toString())
            }
            Log.d("async 异常:", "继续执行后续代码")
        }
    

    运行结果:不会崩溃,日志如下

        2022-03-22 20:02:31.121 27083-27166/com.example.coroutinestest D/async 异常:: 开始准备抛出异常
        2022-03-22 20:02:32.134 27083-27167/com.example.coroutinestest D/async 异常: 捕获的异常-: java.lang.NullPointerException: 自定义空指针异常
        2022-03-22 20:02:32.134 27083-27167/com.example.coroutinestest D/async 异常:: 继续执行后续代码
        2022-03-22 20:02:32.135 27083-27166/com.example.coroutinestest D/async 异常:: 异常被内部CoroutineExceptionHandler处理掉了
    
    

    看到了代码即使是抛出了异常,但是被内部消耗了,并缺不会引起程序崩溃

    前面提到了launch启动的根协程,是不会传播异常的

    这里我们继续印证这个结论:

    例子1:

       private fun loadData1() = scope1.launch {
            try {
                throw NullPointerException("自定义空指针异常")
            } catch (e: Exception) {
                Log.d("try catch捕获的异常:", e.toString())
            }
        }
    
    
    结果:不会崩溃
    
    

    例子2:

     private fun loadData1() = try {
            scope1.launch {
                throw NullPointerException("自定义空指针异常")
            }
        } catch (e: Exception) {
            Log.d("try catch捕获的异常:", e.toString())
        }
        
    
    结果:会崩溃
    
    

    例子3:

        private fun doWork1() = scope1.launch { throw NullPointerException("自定义空指针异常") }
    
    
        private fun loadData1() = scope1.launch {
            try {
                doWork1()
            } catch (e: Exception) {
                Log.d("try catch捕获的异常:", e.toString())
            }
        }
        
    结果:会崩溃
    
    

    ==从例1与例2可以看出异常在协程内部可以被捕获,但是在外部不能被捕获,这里印证了launch不向外抛出异常的结论,再从例3与例1对比可以看出这个协程,这个协程并不是只有根协程才不向线程抛出异常,而是只要launch开启的协程,无论是根还是子都不会向线程中抛出异常==

    同样可以使用上诉方法来解决这个崩溃问题:

    private val job2: Job = Job()
        private val scope2 = CoroutineScope(Dispatchers.Default + job2)
    
        private fun loadData2() = scope2.launch(CoroutineExceptionHandler { _, exception ->
            {
                Log.d("Handler捕获的异常", exception.toString())
            }
        }) {
            try {
                //无论launch有几层都不会崩溃
                launch { launch { throw NullPointerException("自定义空指针异常") } }
            } catch (e: Exception) {
                Log.d("try catch捕获的异常:", e.toString())
            }
        }
    

    再来印证前面所说的:CoroutineExceptionHandler只能处理当前域内开启的子协程或者当前协程抛出的异常

    运行下面的代码:

     private val job3: Job = Job()
        private val scope3 = CoroutineScope(Dispatchers.Default + job3)
    
        private fun doWork3() = scope3.launch { throw NullPointerException("自定义空指针异常") }
    
        private fun loadData3() = scope3.launch(CoroutineExceptionHandler { _, exception ->
            {
                Log.d("Handler捕获的异常", exception.toString())
            }
        }) {
            try {
                doWork3()
            } catch (e: Exception) {
                Log.d("try catch捕获的异常:", e.toString())
            }
        }
    

    结果是:崩溃
    因为doWork3方法开启的协程不是在当前域下开启的协程而是scope3开启的,只是在当前域下运行而已,这里就印证了上面的说法

    但是可以通过增加一个CoroutineExceptionHandler来解决上面的问题,代码如下:

     private val job4: Job = Job()
        private val scope4 =
            CoroutineScope(Dispatchers.Default + job4 + CoroutineExceptionHandler { _, exception ->
                {
                    Log.d("Handler捕获的异常", exception.toString())
                }
            })
    
        //无论launch有几层都不会崩溃
        private fun doWork4() = scope4.launch { launch { throw NullPointerException("自定义空指针异常") } }
    
        private fun loadData4() = scope4.launch {
            try {
                doWork4()
            } catch (e: Exception) {
                Log.d("try catch捕获的异常:", e.toString())
            }
        }
    

    结果是:不会崩溃

    supervisorScope 和 SupervisorJob

    前面讲到了CoroutineExceptionHandler可以捕获异常并且处理掉异常,程序不会崩溃,这里还有一种方式就是使用supervisorScope 和 SupervisorJob

    supervisorScope 和 SupervisorJob的原理是:将异常不传播给自己的父协程

    首先我们来看一个例子:

      private val handler7 = CoroutineExceptionHandler { _, _ ->
            Log.d("kobe", "CoroutineExceptionHandler")
        }
    
        private fun coroutineBuildRunBlock7() = runBlocking(Dispatchers.IO) {
            CoroutineScope(Job() + handler7)
                .launch {
                    launch {
                        Log.d("kobe", "start job1 delay")
                        delay(1000)
                        Log.d("kobe", "end job1 delay")
                    }
                    launch {
                        Log.d("kobe", "job2 throw execption")
                        throw NullPointerException()
                    }
                }
        }
    

    结果是:不崩溃,日志如下

    2022-03-22 15:24:34.022 20373-20411/com.example.coroutinestest D/kobe: start job1 delay
    2022-03-22 15:24:34.025 20373-20412/com.example.coroutinestest D/kobe: job2 throw execption
    2022-03-22 15:24:34.029 20373-20412/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
    

    看到一个现象就是:子协程崩溃会引起兄弟协程的执行错误,这就是文章前面所说的取消其他子协程,这当然不是我们想看到的情况,互不影响才是最优解,所以有了下面的方法:

      private val handler8 = CoroutineExceptionHandler { _, _ ->
            Log.d("kobe", "CoroutineExceptionHandler")
        }
    
        private fun coroutineBuildRunBlock8() = runBlocking(Dispatchers.IO) {
            CoroutineScope(Job() + handler8)
                .launch {
                    launch {
                        delay(2000)
                        Log.d("kobe", "start job3 delay")
                    }
                    supervisorScope {
                        launch {
                            Log.d("kobe", "start job1 delay")
                            delay(1000)
                            Log.d("kobe", "end job1 delay")
                        }
                        launch {
                            Log.d("kobe", "job2 throw execption")
                            throw NullPointerException()
                        }
                    }
                }
        }
    
    

    结果是:不崩溃,日志如下

     2022-03-22 15:48:07.384 21777-21818/com.example.coroutinestest D/kobe: start job1 delay
    2022-03-22 15:48:07.384 21777-21820/com.example.coroutinestest D/kobe: job2 throw execption
    2022-03-22 15:48:07.385 21777-21820/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
    2022-03-22 15:48:08.391 21777-21818/com.example.coroutinestest D/kobe: end job1 delay
    2022-03-22 15:48:09.389 21777-21818/com.example.coroutinestest D/kobe: start job3 delay
    
    按照前面的逻辑异常捕获了,使用了supervisorScope所以一个子协程的异常不会会影响另一个子协程的运行,并且不会影响这个域外的兄弟协程,所以日志全
    

    所以supervisorScope中开启协程,无论多少个子协程都互不影响,这是我们想要的处理情况

    那我们再来看下SupervisorJob,运行下面代码:

    private val supervisorJob9 = SupervisorJob()
        private val handler9 = CoroutineExceptionHandler { _, _ ->
            Log.d("kobe", "CoroutineExceptionHandler")
        }
         private val handler99 = CoroutineExceptionHandler { _, _ ->
            Log.d("kobe", "顶层异常处理")
        }
    
    
        private fun coroutineBuildRunBlock9() = runBlocking(Dispatchers.IO) {
    
            CoroutineScope(handler99 ).launch {
                CoroutineScope(  handler9+supervisorJob9)
                    .launch {
                        launch {
                            Log.d("kobe", "start job1 delay")
                            delay(1000)
                            Log.d("kobe", "end job1 delay")
                        }
                        launch {
                            Log.d("kobe", "job2 throw execption")
                            throw NullPointerException()
                        }
                    }
            }
        }
    
    
    

    结果是:不会崩溃,日志如下

    2022-03-23 17:32:25.771 8593-8638/com.example.coroutinestest D/kobe: job2 throw execption
    2022-03-23 17:32:25.772 8593-8642/com.example.coroutinestest D/kobe: start job1 delay
    2022-03-23 17:32:25.785 8593-8642/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
    

    我们这次来分析日志,日志中没有“顶层异常处理”所以这个异常肯定就没有传播出去,也没有打出“end job1 delay”来表示影响了这个协程内部的兄弟协程

    所以结论是: ==SupervisorJob这个任务是阻止异常不会向外传播,因此不会影响其父亲/兄弟协程,也不会被其兄弟协程抛出的异常影响,但是他内部生成的各种协程是依然会像job一样互相影响,并且这个异常必须使用CoroutineExceptionHandler处理掉,不然会引起程序崩溃==

    看到这里可能又有人会问这个很正常,因为异常被handler9处理掉了,所以就没有传递到父亲协程,那这里我们可以这样处理,我们去掉这个handler9:

       private fun coroutineBuildRunBlock9() = runBlocking(Dispatchers.IO) {
    
            CoroutineScope(handler99 ).launch {
                CoroutineScope(supervisorJob9)
                    .launch {
                        launch {
                            Log.d("kobe", "start job1 delay")
                            delay(1000)
                            Log.d("kobe", "end job1 delay")
                        }
                        launch {
                            Log.d("kobe", "job2 throw execption")
                            throw NullPointerException()
                        }
                    }
            }
        }
    

    结果:程序崩溃,并且没有打印出“顶层异常处理”,所以前面的结论是正确的

    我们再来印证以下兄弟协程是否被影响,运行代码:

      private val supervisorJob10 = SupervisorJob()
        private val handler10 = CoroutineExceptionHandler { _, _ ->
            Log.d("kobe", "CoroutineExceptionHandler")
        }
    
        private val coroutineContext10 = handler10 + supervisorJob10
    
    
        private fun coroutineBuildRunBlock10() = runBlocking(Dispatchers.IO) {
            CoroutineScope(coroutineContext10)
                .launch {
                    launch {
                        Log.d("kobe", "start job1 delay")
                        delay(1000)
                        Log.d("kobe", "end job1 delay")
                    }
                    launch {
                        Log.d("kobe", "start job2 delay")
                        delay(1000)
                        Log.d("kobe", "end job2 delay")
                    }
    
                    CoroutineScope(coroutineContext10).launch {
                        launch {
                            Log.d("kobe", "start job3 delay")
                            delay(1000)
                            Log.d("kobe", "end job3 delay")
                        }
                        launch {
                            Log.d("kobe", "job4 throw execption")
                            throw NullPointerException()
                        }
                    }
                }
        }
    
    

    结果是:不会崩溃,日志如下

    2022-03-22 15:45:20.807 21611-21653/com.example.coroutinestest D/kobe: start job1 delay
    2022-03-22 15:45:20.809 21611-21652/com.example.coroutinestest D/kobe: start job2 delay
    2022-03-22 15:45:20.814 21611-21651/com.example.coroutinestest D/kobe: start job3 delay
    2022-03-22 15:45:20.815 21611-21654/com.example.coroutinestest D/kobe: job4 throw execption
    2022-03-22 15:45:20.817 21611-21654/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
    2022-03-22 15:45:21.820 21611-21654/com.example.coroutinestest D/kobe: end job1 delay
    2022-03-22 15:45:21.820 21611-21651/com.example.coroutinestest D/kobe: end job2 delay
    

    结果是:兄弟协程并不影响,前面的结论正确

    结论

    **1. try-catch 只能捕捉当前线程的堆栈信息。对于非当前线程无法实现捕捉

    1. launch启动的根协程,是不会传播异常的
    2. async开启一个根协程或者子协程,异常都会被抛出给线程,并且可以被try-catch捕获到。async开启一个根协程,在调用await方法时候会抛出异常,这个异常可以用try-catch捕获不引起崩溃,如果这个协程不是根协程,那么是代码执行到 throw 异常的时候就抛出了异常与是否调用await方法无关这个异常可以用try-catch捕获但是会引起崩溃,可以用CoroutineExceptionHandler进行捕获解决崩溃问题
    3. CoroutineExceptionHandler的官方解释是:处理协程抛出的异常的函数,官方又一个隐藏点没说就是这个CoroutineExceptionHandler只能处理当前域内开启的子协程或者当前协程抛出的异常
    4. SupervisorJob这个任务是阻止异常不会向外传播,因此不会影响其父亲/兄弟协程,也不会被其兄弟协程抛出的异常影响,但是他内部生成的各种协程是依然会像job一样互相影响,并且这个异常必须使用CoroutineExceptionHandler处理掉,不然会引起程序崩溃**

    最后

    协程的异常处理是很复杂的一个过程,里面融合了结构化并发的思想,这个开发思想伴随了kotlin的后续开发,并且协程的异常处理中有很多坑需要一一去踩,在官方文档与网上的零散碎片知识中很难找到这些坑点,如果能认真看完上诉的讲解,肯定对协程的异常有了一个新的认知,更希望读者将上面的案例放在自己的代码中去运行总结,若有不对的地方欢迎指出改正

    相关文章

      网友评论

        本文标题:全网最详细的Kotlin协程-异常篇讲解与踩坑

        本文链接:https://www.haomeiwen.com/subject/lexhjrtx.html