安卓协程异步任务实践

99封情书 提交于 2020-01-25 03:21:44

使用协程的过程中,最常遇到的就是处理异步任务,先来前期的一些动作。设置BaseActivity
这里为什么要用覆写上下文的方式,是因为这里可以加入统一的异常 handler处理,但请注意,这里的handler 只适用于处理launch 的协程,async的协程异常处理参看最后

open class BaseCorountineActivty : AppCompatActivity(),  CoroutineScope {
    //统一处理协程中异常的报错
    val handler = CoroutineExceptionHandler { _, e ->
        println("协程任务  顶层异常处理")
    }


    override fun onDestroy() {
        cancel()
        super.onDestroy()
    }

    override val coroutineContext: CoroutineContext
        get() = SupervisorJob() + Dispatchers.Main + handler
}

然后是测试的Activity, compute挂起函数模拟异步耗时任务

class TestActivity : BaseCorountineActivty() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //统一处理协程中异常的报错
        val handler = CoroutineExceptionHandler { _, e ->
            println("协程任务  已被处理")
        }

        for (i in 1..5) {
        	//如果是网络任务需要切换调度器launch(Dispatchers.IO)
            launch {
                compute(i)

            }
        }
    }


    private suspend fun compute(i: Int): Int {
        if(i == 3) throw RuntimeException("Erro")
        println("协程任务${i} 开始")
        delay(500 + (i * 1000).toLong())
        println("协程任务 内容值${i}")
        println("协程任务${i} 结束")
        return i
    }
}

模拟并发的异步任务

上面的代码是打印任务,在值等于3的时候 模拟异常发生,直接执行以上代码 ,结果如下,纳尼,这就并发异步执行了?抛出的异常被 BaseCorountineActivty 获取,十分完美,

协程任务1 开始           
协程任务2 开始           
协程任务  顶层异常处理       
协程任务4 开始           
协程任务5 开始           
协程任务 内容值1          
协程任务1 结束           
协程任务 内容值2          
协程任务2 结束           
协程任务 内容值4          
协程任务4 结束           
协程任务 内容值5          
协程任务5 结束             

模拟类似IntentService那种按顺序的异步

将for循环中的内容处理变动一下,如下, 每个协程任务用job.join(),这里的join是啥意思呢?看官方的解释
,大意是,

  • 挂起协程,去执行job中的任务直到完成。再恢复调用协程(无异常的情况下)。此时协程依旧处于 [active]状态.
  • 这个函数会启动仍然在new state 状态下的job对应的协程任务。
  • 当所有子任务完成时,这个job才算完成。
  • 这个挂起函数时可以取消的,并 总是会检测调用协程任务的取消情况。(译者感觉意思是即使在执行,你依然可以调用cancel 来取消)
  • 如果调用子任务的协程被取消了,或者当你去调用的时候发现挂起函数已经完成,那么会抛出一个CancellationException
  • 特别要注意的是,父协程在子协程(此子协程已经用launch开始运行了)里面调用join,如果子协程崩溃了,会抛出一个 [CancellationException],这种情况下,除非上下文中有加入自定义的 CoroutineExceptionHandler ,才能抓住这个抛出的异常
  • 这个函数可以用于 [onJoin]语句的select调用的情形。
  • 可以使用isCompleted来无延迟的检查job的完成情况,
  • 还有一个函数cancelAndJoin可以用来执行先cancel在join的动作 。(译者感觉可以用在那些防抖动的场合,)

/**
* Suspends the coroutine until this job is complete. This invocation resumes normally (without exception)
* when the job is complete for any reason and the [Job] of the invoking coroutine is still [active][isActive].
* This function also [starts][Job.start] the corresponding coroutine if the [Job] was still in new state.
*
* Note that the job becomes complete only when all its children are complete.
*
* This suspending function is cancellable and always checks for a cancellation of the invoking coroutine’s Job.
* If the [Job] of the invoking coroutine is cancelled or completed when this
* suspending function is invoked or while it is suspended, this function
* throws [CancellationException].
*
* In particular, it means that a parent coroutine invoking join on a child coroutine that was started using
* launch(coroutineContext) { ... } builder throws [CancellationException] if the child
* had crashed, unless a non-standard [CoroutineExceptionHandler] is installed in the context.
*
* This function can be used in [select] invocation with [onJoin] clause.
* Use [isCompleted] to check for a completion of this job without waiting.
*
* There is [cancelAndJoin] function that combines an invocation of [cancel] and join.
*/

class TestActivity : BaseCorountineActivty() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //统一处理协程中异常的报错
        val handler = CoroutineExceptionHandler { _, e ->
            println("协程任务  已被处理")
        }

        launch {
            for (i in 1..5) {
                var job = launch {
                    compute(i)
                }

                job.join()
            }
        }
        
    }


    private suspend fun compute(i: Int): Int {
        if(i == 3) throw RuntimeException("Erro")
        println("协程任务${i} 开始")
        delay(500 + (i * 1000).toLong())
        println("协程任务 内容值${i}")
        println("协程任务${i} 结束")
        return i
    }
}

很不幸,在i == 3 的时候发生异常,你会发现整个后面的任务都挂了,用launch启动的协程,如果有一个任务失败异常,会影响该scope范围内的所有协程都挂掉,按官方的解释,launch中的异常是从子向父抛出,会导致父挂掉。结果此父中的所有子任务也因此挂了,即使我们再BaseActivity中设置的上下文中定义了SupervisorJob()也不行。 但如果有类似需求,一个任务有N步,必须每步都成功才能最后执行,这就是你的菜。

协程任务1 开始       
协程任务 内容值1      
协程任务1 结束       
协程任务2 开始       
协程任务 内容值2      
协程任务2 结束       
协程任务  顶层异常处理   

如果期望发生异常的任务不影响其他任务,应该怎么弄?协程中有个scope叫做supervisorScope ,其中的子任务如果报异常,不影响其他协程的任务,他抛异常是从上往下,不影响它的父级和兄弟级,只影响子级。代码更改如下。

class TestActivity : BaseCorountineActivty() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //统一处理协程中异常的报错
        val handler = CoroutineExceptionHandler { _, e ->
            println("协程任务  当前activity处理")
        }

        launch {
            for (i in 1..5) {
                supervisorScope {
                    var job = launch {
                        compute(i)
                    }

                    job.join()
                }

            }
        }

    }


    private suspend fun compute(i: Int): Int {
        if(i == 3) throw RuntimeException("Error")
        println("协程任务${i} 开始")
        delay(500 + (i * 1000).toLong())
        println("协程任务 内容值${i}")
        println("协程任务${i} 结束")
        return i
    }
}

得到我们期望的结果,顺序执行异步任务,当任务3 抛异常的时候,不影响后面的子任务,并且任务3的异常被顶层异常处理捕获。

 I/System.out: 协程任务1 开始              
 I/System.out: 协程任务 内容值1             
 I/System.out: 协程任务1 结束              
 I/System.out: 协程任务2 开始              
 I/System.out: 协程任务 内容值2             
 I/System.out: 协程任务2 结束              
 I/System.out: 协程任务  顶层异常处理          
 I/System.out: 协程任务4 开始              
 I/System.out: 协程任务 内容值4             
 I/System.out: 协程任务4 结束              
 I/System.out: 协程任务5 开始              
 I/System.out: 协程任务 内容值5             
 I/System.out: 协程任务5 结束              

以上我们一直在使用launch来启动协程,实际上我们知道还有一个叫async的函数,可以表示异步并发的处理,我们来试一下, async可以通过返回一个 实现 Deferred接口 的 对象,Deferred 又实现了 Job的接口,Deferred 可以通过await 来获取计算出来的值,那这个await 是啥意思,我们看一下文档 ,
大意是,

  • await 在任务计算完成时,await 会等待任务结果的返回值,并不会阻塞线程。
  • 可以返回正确的结果,有异常时则抛异常
  • 这个挂起函数时可以cancel 的
  • 如若挂起函数还在等待,但是协程已经调用cancel 或者执行完毕,那么会抛出CancellationException 。
  • 这个函数可以用于 [onAwait]语句的select调用的情形。
  • 可以使用isCompleted来无延迟的检查job的完成情况,

/**
* Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete,
* returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
*
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
* immediately resumes with [CancellationException].
*
* This function can be used in [select] invocation with [onAwait] clause.
* Use [isCompleted] to check for completion of this deferred value without waiting.
*/

执行代码如下,期望是并发异步执行,并且由于有supervisorScope ,期望是错误的不影响其他,走一波看看

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //统一处理协程中异常的报错
        val handler = CoroutineExceptionHandler { _, e ->
            println("协程任务  当前activity处理")
        }

        launch {
            for (i in 1..5) {
                supervisorScope {
                    var job = async {
                        compute(i)
                    }
                    job.await()
                }

            }
        }
    }

最后发现居然是阻塞执行,而且一旦发生异常,其他协程任务就挂掉了

I/System.out: 协程任务1 开始      
I/System.out: 协程任务 内容值1     
I/System.out: 协程任务1 结束      
I/System.out: 协程任务2 开始      
I/System.out: 协程任务 内容值2     
I/System.out: 协程任务2 结束      
I/System.out: 协程任务  顶层异常处理  

我们先把可能发生异常的任务跳过,换一个代码,换一种写法试试,这次不用for循环了,展开,并且同时去获取值

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //统一处理协程中异常的报错
        val handler = CoroutineExceptionHandler { _, e ->
            println("协程任务  当前activity处理")
        }

        launch {
            var job1 = async { compute(1) }
            var job2 = async { compute(2) }
//            var job3 = async { compute(3) }
            var job4 = async { compute(4) }
            var job5 = async { compute(5) }

            job1.await() + job2.await() + job4.await() + job5.await()
        }
    }

终于异步并发了,

I/System.out: 协程任务1 开始       
I/System.out: 协程任务2 开始       
I/System.out: 协程任务4 开始       
I/System.out: 协程任务5 开始       
I/System.out: 协程任务 内容值1      
I/System.out: 协程任务1 结束       
I/System.out: 协程任务 内容值2      
I/System.out: 协程任务2 结束       
I/System.out: 协程任务 内容值4      
I/System.out: 协程任务4 结束       
I/System.out: 协程任务 内容值5      
I/System.out: 协程任务5 结束       

如果这时候加入会异常的 任务3,会怎么样呢?

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //统一处理协程中异常的报错
        val handler = CoroutineExceptionHandler { _, e ->
            println("协程任务  当前activity处理")
        }

        launch {
            var job1 = async { compute(1) }
            var job2 = async { compute(2) }
            var job3 = async { compute(3) }
            var job4 = async { compute(4) }
            var job5 = async { compute(5) }

            job1.await() + job2.await() + job3.await() + job4.await() + job5.await()
        }
    }

结果如下, 一旦碰到异常,其他该scope的协程任务也挂了。

I/System.out: 协程任务1 开始       
I/System.out: 协程任务2 开始       
I/System.out: 协程任务  顶层异常处理   

我们加入supervisorScope 试试看,能否阻止异常范围的扩大

launch {
            supervisorScope {
                var job1 = async { compute(1) }
                var job2 = async { compute(2) }
                var job3 = async { compute(3) }
                var job4 = async { compute(4) }
                var job5 = async { compute(5) }

                job1.await() + job2.await() + job3.await() + job4.await() + job5.await()
            }

        }

supervisorScope 的确保护了先于任务3 运行的任务1 和任务2 完成,但是任务4 和任务5 由于异常的发生,开始后就没结果了,执行不下去了。实际需求也是这样,如果你的任务需要合并n个子任务的结果,但一旦某个出错,整个就不该执行下去,但有时需要已经成功的几步的结果,所以这也是有使用场景的。

I/System.out: 协程任务1 开始       
I/System.out: 协程任务2 开始       
I/System.out: 协程任务4 开始       
I/System.out: 协程任务5 开始       
I/System.out: 协程任务 内容值1      
I/System.out: 协程任务1 结束       
I/System.out: 协程任务 内容值2      
I/System.out: 协程任务2 结束       
I/System.out: 协程任务  顶层异常处理   

写到这里还有一个问题,那当async 的返回结果没有依赖关系,怎么能实现向lauch那样不影响后续任务呢,答案是有的 我们还可以 try catch

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //统一处理协程中异常的报错
        val handler = CoroutineExceptionHandler { _, e ->
            println("协程任务  当前activity处理")
        }

        launch {
           supervisorScope {
               var job1 = async { compute(1) }
               var job2 = async { compute(2) }
               var job3 = async { compute(3) }
               var job4 = async { compute(4) }
               var job5 = async { compute(5) }


               job1.await()
               job2.await()
               try {
                   job3.await()
               } catch (e: Exception) {
                   println("协程任务  异常的任务被try catch了")
               }
               job4.await()
               job5.await()
           }
        }
    }

结果如下, 可以看到,如果有多个异步任务,某个可能出错又不想影响其他的协程子任务,,那可能所有的await 你都要 try catch , 如果是这种情况,那么最初那种用launch的方式反而是最好的选择了。但记得 如果是网络或者后台网络任务,需要指定一下调度器launch(Dispatchers.IO)

I/System.out: 协程任务1 开始                       
I/System.out: 协程任务2 开始                       
I/System.out: 协程任务4 开始                       
I/System.out: 协程任务5 开始                       
I/System.out: 协程任务 内容值1                      
I/System.out: 协程任务1 结束                       
I/System.out: 协程任务 内容值2                      
I/System.out: 协程任务2 结束                       
I/System.out: 协程任务  异常的任务被try catch了         
I/System.out: 协程任务 内容值4                      
I/System.out: 协程任务4 结束                       
I/System.out: 协程任务 内容值5                      
I/System.out: 协程任务5 结束                       

那么async 启动的任务就没有办法了么?还是有的,可以扩展一个函数,替代await, 比如下面的方法,定义一个

在BaseActivity中定义一个抽象接口来处理,并给job.await() 返回的 Deferred接口定义一个扩展方法。awaitEx, 默认传入一个匿名内部类来统一处理错误。不传的话就用默认参数,如果有具体情况需要单独处理的,也可以传一个自定义的接口来处理,问题就解决了,有统一性也有单独处理的灵活性。

open class BaseCorountineActivty : BaseActivity(),  CoroutineScope {
    //统一处理协程中异常的报错
    val handler = CoroutineExceptionHandler { _, e ->
        println("协程任务  顶层异常处理")
    }


    override fun onDestroy() {
        cancel()
        super.onDestroy()
    }

    override val coroutineContext: CoroutineContext
        get() = SupervisorJob() + Dispatchers.Main + handler


    suspend fun <T> Deferred<out T>.awaitEx(handle : HandleError = object : HandleError {
        override fun onError(e: Throwable) {
            //统一处理异常
            println("协程任务  统一处理")
        }

    }) : T?  {
        try {
            return this.await()
        } catch (e : Throwable) {
            handle.onError(e)
            return null
        }
    }

    interface HandleError {
        fun onError(e : Throwable)
    }
}
launch {
            supervisorScope {
                var job1 = async { compute(1) }
                var job2 = async { compute(2) }
                var job3 = async { compute(3) }
                var job4 = async { compute(4) }
                var job5 = async { compute(5) }


                var y : Int =  job1.await()
                job2.awaitEx()
                var x : Int? = job3.awaitEx(object : HandleError {
                    override fun onError(e: Throwable) {
                        print("我不要统一处理,我要自己处理")
                    }
                })
                job4.await()
                job5.await()
            }
        }

结果如下

协程任务1 开始              
协程任务2 开始              
协程任务4 开始              
协程任务5 开始              
协程任务 内容值1             
协程任务1 结束              
协程任务 内容值2             
协程任务2 结束              
协程任务 内容值4             
协程任务4 结束              
协程任务 内容值5             
协程任务5 结束              
协程任务 我不要统一处理,我要自己处理   
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!