安卓協程異步任務實踐

使用協程的過程中,最常遇到的就是處理異步任務,先來前期的一些動作。設置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 結束              
協程任務 我不要統一處理,我要自己處理   
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章