Android版kotlin協程入門(三):kotlin協程的異常處理

南方吳彥祖_藍斯發表於2021-10-11

kotlin協程的異常處理:

在上一篇 《Android kotlin協程入門(二):kotlin協程的關鍵知識點初步講解》中我們提到這節將會講解協程的異常處理。

但是筆者在寫這篇文章的時候遇到了些問題,主要是講解的深度怎麼去把控,因為要處理異常,首先得知道異常是如何產生,那麼必然就涉及到協程 建立->啟動->執行->排程->恢復->完成(取消)流程。這其中每一步都能羅列出一堆需要講解東西,所以筆者最終決定,我們在這章節中只檢視關鍵點位置,其中涉及到的一些跳出關鍵點的位置,我們只做一個基本提點,不做延伸。

當然基於前兩篇文章的反饋,有讀者提到文章文字和程式碼資訊太多,從頭到尾看下來很累,想讓筆者中間安排一些騷圖緩解下緊張的學習氣氛。

Android版kotlin協程入門(三):kotlin協程的異常處理

所以筆者在這篇文章中嘗試加入一些元素,如果有不合適的地方,麻煩批評指正。

協程異常的產生流程

我們在開發Android應用時,出現未捕獲的異常就會導致程式退出。同樣的協程出現未捕獲異常,也會導致應用退出。我們要處理異常,那就得先看看協程中的異常產生的流程是什麼樣的,協程中出現未捕獲的異常時會出現哪些資訊,如下:

private fun testCoroutineExceptionHandler(){
   GlobalScope.launch {
       val job = launch {
            Log.d("${Thread.currentThread().name}", " 丟擲未捕獲異常")            throw NullPointerException("異常測試")
        }
       job.join()
       Log.d("${Thread.currentThread().name}", "end")
    }
}
複製程式碼

我們丟擲了一個 NullPointerException異常但沒有去捕獲,所以會導致了應用崩潰退出。

D/DefaultDispatcher-worker-2:  丟擲未捕獲異常
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: com.carman.kotlin.coroutine, PID: 22734
    java.lang.NullPointerException: 異常測試
        at com.carman.kotlin.coroutine.MainActivity$testException$1$job$1.invokeSuspend(MainActivity.kt:251)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
複製程式碼

我們看到這個異常是在在 CoroutineScheduler中產生的,雖然我們不知道 CoroutineScheduler是個什麼東西。但是我們可以從日誌上執行的方法名稱先大概的分析一下流程:

它先是建立一個 CoroutineScheduler的一個 Worker物件,接著執行 Worker物件的 run方法,然後 runWorker方法呼叫了 executeTask,緊接著又在 executeTask裡面執行了 runSafely,再接著透過 runSafely執行了 DispatchedTaskrun方法,最後 DispatchedTask.run呼叫了 continuationresumeWith方法, resumeWith方法中在執行 invokeSuspend的時候丟擲了異常。

再來個通熟一點的,你們應該就能猜出大概意思來。僱主先是找包工頭 CoroutineScheduler要了一個工人 Worker,然後給這個工人安排了一個搬磚任務 DispatchedTask,同時告訴這個工人他要安全 runSafely的搬磚,然後僱主就讓工人 Worker開始工作 runWorker,工人 Worker就開始執行 executeTask僱主吩咐的任務 DispatchedTask,最後透過 resumeWith來執行 invokeSuspend的時候告訴僱主出現了問題(丟擲了異常).

Android版kotlin協程入門(三):kotlin協程的異常處理

彆著急,仔細想一想,有沒有發現這個跟 ThreadPoolExecutor執行緒池和 Thread執行緒的執行很像。包工頭就像是 ThreadPoolExecutor執行緒池,工人就是 Thread執行緒。

我們透過執行緒池( CoroutineScheduler)建立了一個 Thread執行緒( Worker),然後開始執行執行緒( runWorker),執行緒裡面透過 executeTask執行一個任務 DispatchedTask,在執行任務的時候我們透過 try..catch來保證任務安全執行 runSafely,然後在 DispatchedTask執行任務的時候,因為執行出現異常,所以在 catch中透過 resumeWith來告知結果執行緒出問題了。咦,邏輯好像突然變得清晰很多。

Android版kotlin協程入門(三):kotlin協程的異常處理

這麼看的話,這個協程異常的產生是不是基本原理就出來了。那麼我們接下里看看是不是正如我們所想的,我們先找到 CoroutineScheduler看看他的實現:

    internal class CoroutineScheduler(...) : Executor, Closeable {        @JvmField
        val globalBlockingQueue = GlobalQueue()        fun runSafely(task: Task) {            try {
                task.run()
            } catch (e: Throwable) {
                val thread = Thread.currentThread()
                thread.uncaughtExceptionHandler.uncaughtException(thread, e)
            } finally {
                unTrackTask()
            }
        }         //省略... 
        internal inner class Worker private constructor() : Thread() {            override fun run() = runWorker()            private fun runWorker() {
                var rescanned = false
                while (!isTerminated && state != WorkerState.TERMINATED) {
                    val task = findTask(mayHaveLocalTasks)                    if (task != null) {
                        rescanned = false
                        minDelayUntilStealableTaskNs = 0L
                        executeTask(task)                        continue
                    } else {
                        mayHaveLocalTasks = false
                    }                    //省略...
                    continue
                }
            }            private fun executeTask(task: Task) {                //省略...
                runSafely(task)               //省略...
            }            fun findTask(scanLocalQueue: Boolean): Task? {                if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
                val task = if (scanLocalQueue) {
                    localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
                } else {
                    globalBlockingQueue.removeFirstOrNull()
                }                return task ?: trySteal(blockingOnly = true)
            }            //省略... 
        }        //省略... 
    }
複製程式碼

哎呀呀,不得了,跟我們上面想的一模一樣。 CoroutineScheduler繼承 ExecutorWorker繼承 Thread,同時 runWorker也是執行緒的 run方法。在 runWorker執行了 executeTask(task),接著在 executeTask呼叫中 runSafely(task),然後我們看到 runSafely使用 try..catch了這個 task任務的執行,最後在 catch中丟擲了未捕獲的異常。那麼很明顯這個task肯定就是我們的 DispatchedTask,那就到這裡結束了麼

很明顯並沒有,我們看到 catch中丟擲的是個執行緒的 uncaughtExceptionHandler,這個我們就很熟了,在Android開發中都是透過這個崩潰資訊。但是這個明顯不是我們這次的目標。

Android版kotlin協程入門(三):kotlin協程的異常處理

繼續往下分析,我們看看這個 task到底是不是 DispatchedTask。回到 executeTask(task)的呼叫出,我們看到這個 task是透過 findTask獲取的,而這個 task又是在 findTask中透過 CoroutineScheduler執行緒池中的 globalBlockingQueue佇列中取出的,我們看看這個 GlobalQueue

internal class GlobalQueue : LockFreeTaskQueue<Task>(singleConsumer = false)
複製程式碼
internal actual typealias SchedulerTask = Task
複製程式碼

我可以看到這個佇列裡面存放的就是 Task,又透過kotlin語言中的typealias給 Task取了一個 SchedulerTask的別名。而 DispatchedTask繼承自 SchedulerTask,那麼 DispatchedTask的來源就解釋清楚了。

internal abstract class DispatchedTask<in T>(
    @JvmField public var resumeMode: Int
) : SchedulerTask() { //省略...
 internal open fun getExceptionalResult(state: Any?): Throwable? =
        (state as? CompletedExceptionally)?.cause
 public final override fun run() {
      assert { resumeMode != MODE_UNINITIALIZED }
      val taskContext = this.taskContext
      var fatalException: Throwable? = null
    try {
            val delegate = delegate as DispatchedContinuation<T>
            val continuation = delegate.continuation
            withContinuationContext(continuation, delegate.countOrElement) {
                val context = continuation.context
                val state = takeState()
                val exception = getExceptionalResult(state)
                val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null                if (job != null && !job.isActive) {
                    val cause = job.getCancellationException()
                    cancelCompletedResult(state, cause)
                    continuation.resumeWithStackTrace(cause)
                } else {                    if (exception != null) {
                        continuation.resumeWithException(exception)
                    } else {
                        continuation.resume(getSuccessfulResult(state))
                    }
                }
            }
        } catch (e: Throwable) {
            fatalException = e
        } finally {
            val result = runCatching { taskContext.afterTask() }
            handleFatalException(fatalException, result.exceptionOrNull())
        }
    }
}
複製程式碼

接著我們繼續看 DispatchedTaskrun方法,前面怎麼獲取 exception 的我們先不管,直接看當 exception 不為空時,透過 continuationresumeWithException返回了異常。我們在上面提到過 continuation,在掛起函式的掛起以後,會透過 Continuation呼叫 resumeWith函式恢復協程的執行,同時返回 Result<T>型別的成功或者失敗。實際上 resumeWithException呼叫的是 resumeWith,只是它是個擴充套件函式,只是它只能返回 Result.failure。同時異常就這麼被 Continuation無情丟擲。

public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
    resumeWith(Result.failure(exception))
複製程式碼

誒,不對啊,我們在這裡還沒有執行 invokeSuspend啊,你是不是說錯了。

Android版kotlin協程入門(三):kotlin協程的異常處理

是滴,這裡只是一種可能,我們現在回到呼叫 continuation的地方,這裡的 continuation在前面透過 DispatchedContinuation得到的,而實際上 DispatchedContinuation是個 BaseContinuationImpl物件( 這裡不擴充套件它是怎麼來的,不然又得從頭去找它的來源)。

  val delegate = delegate as DispatchedContinuation<T>
  val continuation = delegate.continuation
複製程式碼
internal abstract class BaseContinuationImpl(    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {     public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result        while (true) {
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!! // fail fast when trying to resume continuation 
                val outcome: Result<Any?> =                    try {
                        val outcome = invokeSuspend(param)                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted() // this state machine instance is terminating
                if (completion is BaseContinuationImpl) {
                    current = completion
                    param = outcome
                } else {
                    completion.resumeWith(outcome)                    return
                }
            }
        }
    }
}
複製程式碼

可以看到最終這裡面 invokeSuspend才是真正呼叫我們協程的地方。最後也是透過 Continuation呼叫 resumeWith函式恢復協程的執行,同時返回 Result<T>型別的結果。和我們上面說的是一樣的,只是他們是在不同階段。

那、那、那、那下面那個 finally它又是有啥用,我們都透過 resumeWithException把異常丟擲去了,為啥下面又還有個 handleFatalException,這貨又是幹啥用的???

handleFatalException主要是用來處理 kotlinx.coroutines庫的異常,我們這裡大致的瞭解下就行了。主要分為兩種:

  1. kotlinx.coroutines庫或編譯器有錯誤,導致的內部錯誤問題。
  2. ThreadContextElement也就是協程上下文錯誤,這是因為我們提供了不正確的 ThreadContextElement實現,導致協程處於不一致狀態。
public interface ThreadContextElement<S> : CoroutineContext.Element {
    public fun updateThreadContext(context: CoroutineContext): S
    public fun restoreThreadContext(context: CoroutineContext, oldState: S)
}
複製程式碼

我們看到 handleFatalException實際是呼叫了 handleCoroutineException方法。 handleCoroutineExceptionkotlinx.coroutines庫中的頂級函式

public fun handleFatalException(exception: Throwable?, finallyException: Throwable?) {    //省略....
    handleCoroutineException(this.delegate.context, reason)
}
複製程式碼
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {    try {
        context[CoroutineExceptionHandler]?.let {
            it.handleException(context, exception)            return
        }
    } catch (t: Throwable) {
        handleCoroutineExceptionImpl(context, handlerException(exception, t))        return
    }
    handleCoroutineExceptionImpl(context, exception)
}
複製程式碼

我們看到 handleCoroutineException會先從協程上下文拿 CoroutineExceptionHandler,如果我們沒有定義的 CoroutineExceptionHandler話,它將會呼叫 handleCoroutineExceptionImpl丟擲一個 uncaughtExceptionHandler導致我們程式崩潰退出。

internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {    for (handler in handlers) {        try {
            handler.handleException(context, exception)
        } catch (t: Throwable) {
            val currentThread = Thread.currentThread()
            currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
        }
    }
    val currentThread = Thread.currentThread()
    currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
複製程式碼

不知道各位是否理解了上面的流程,筆者最開始的時候也是被這裡來來回回的。繞著暈乎乎的。如果沒看懂的話,可以休息一下,揉揉眼睛,倒杯熱水,再回過頭捋一捋。

Android版kotlin協程入門(三):kotlin協程的異常處理

好滴,到此處為止。我們已經大概的瞭解kotlin協程中異常是如何丟擲的,下面我們就不再不過多延伸。下面我們來說說異常的處理。

協程的異常處理

kotlin協程異常處理我們要分成兩部分來看,透過上面的分解我們知道一種異常是透過 resumeWithException丟擲的,還有一種異常是直接透過 CoroutineExceptionHandler丟擲,那麼我們現在就開始講講如何處理異常。

第一種:當然就是我們最常用的 try..catch啦,只要有異常崩潰我就先 try..catch下,先不管流程對不對,我先保住我的程式不能崩潰。

Android版kotlin協程入門(三):kotlin協程的異常處理
private fun testException(){
    GlobalScope.launch{
        launch(start = CoroutineStart.UNDISPATCHED) {
            Log.d("${Thread.currentThread().name}", " 我要開始拋異常了")            try {                throw NullPointerException("異常測試")
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        Log.d("${Thread.currentThread().name}", "end")
    }
}
複製程式碼
D/DefaultDispatcher-worker-1:  我要開始拋異常了
W/System.err: java.lang.NullPointerException: 異常測試
W/System.err:     at com.carman.kotlin.coroutine.MainActivity$testException$1$1.invokeSuspend(MainActivity.kt:252)
W/System.err:     at com.carman.kotlin.coroutine.MainActivity$testException$1$1.invoke(Unknown 
//省略...
D/DefaultDispatcher-worker-1: end
複製程式碼

誒嘿,這個時候我們程式沒有崩潰,只是輸出了警告日誌而已。那如果遇到 try..catch搞不定的怎麼辦,或者遺漏了需要 try..catch的位置怎麼辦。比如:

private fun testException(){    var a:MutableList<Int> = mutableListOf(1,2,3)
    GlobalScope.launch{
       launch {
            Log.d("${Thread.currentThread().name}","我要開始拋異常了" )            try {
                launch{
                    Log.d("${Thread.currentThread().name}", "${a[1]}")
                }
                a.clear()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        Log.d("${Thread.currentThread().name}", "end")
    }
}
複製程式碼
D/DefaultDispatcher-worker-1: endD/DefaultDispatcher-worker-2: 我要開始拋異常了
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-2
    Process: com.carman.kotlin.coroutine, PID: 5394
    java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
        at java.util.ArrayList.get(ArrayList.java:437)
        at com.carman.kotlin.coroutine.MainActivity$testException$1$1$1.invokeSuspend(MainActivity.kt:252)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
複製程式碼
Android版kotlin協程入門(三):kotlin協程的異常處理

當你以為使用 try..catch就能捕獲的時候,然而實際並沒有。這是因為我們的 try..catch使用方式不對,我們必須在使用 a[1]時候再用 try..catch捕獲才行。那就有人會想那我每次都記得使用 try..catch就好了。

是,當然沒問題。但是你能保證你每次都能記住嗎,你的同一戰壕裡的戰友會記住嗎。而且當你的邏輯比較複雜的時候,你使用那麼多 try..catch你程式碼閱讀性是不是降低了很多後,你還能記住哪裡有可能會出現異常嗎。

Android版kotlin協程入門(三):kotlin協程的異常處理

這個時候就需要使用協程上下文中的 CoroutineExceptionHandler。我們在上一篇文章講解協程上下文的時候提到過,它是協程上下文中的一個 Element,是用來捕獲協程中未處理的異常。

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
    public fun handleException(context: CoroutineContext, exception: Throwable)
}
複製程式碼

我們稍作修改:

private fun testException(){
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} :$throwable")
    }
     GlobalScope.launch(CoroutineName("異常處理") + exceptionHandler){
         val job = launch{
             Log.d("${Thread.currentThread().name}","我要開始拋異常了" )
             throw NullPointerException("異常測試")
         }
         Log.d("${Thread.currentThread().name}", "end")
     }
}
複製程式碼
D/DefaultDispatcher-worker-1: 我要開始拋異常了
D/exceptionHandler: CoroutineName(異常處理) :java.lang.NullPointerException: 異常測試
D/DefaultDispatcher-worker-2: end複製程式碼

這個時候即使我們沒有使用 try..catch去捕獲異常,但是異常還是被我們捕獲處理了。是不是感覺異常處理也沒有那麼難。那如果按照上面的寫,我們是不是得在每次啟動協程的時候,也需要跟 try..catch一樣都需要加上一個 CoroutineExceptionHandler呢? 這個時候我們就看出來,各位是否真的有吸收前面講解的知識:

  • 第一種:我們上面講解的 協程作用域部分你已經消化吸收,那麼恭喜你接下來的你可以大概的過一遍或者選擇跳過了。因為接下來的部分和 協程作用域中說到的內容大體一致。

  • 第二種:除第一種的,都是第二種。那你接下來你就得認證仔細的看了。

我們之前在講到 協同作用域主從(監督)作用域的時候提到過,異常傳遞的問題。我們先來看看 協同作用域:

  • 協同作用域如果子協程丟擲未捕獲的異常時,會將異常傳遞給父協程處理,如果父協程被取消,則所有子協程同時也會被取消。

容我盜個官方圖

預設情況下,當協程因出現異常失敗時,它會將異常傳播到它的父級,父級會取消其餘的子協程,同時取消自身的執行。最後將異常在傳播給它的父級。當異常到達當前層次結構的根,在當前協程作用域啟動的所有協程都將被取消。

Android版kotlin協程入門(三):kotlin協程的異常處理

我們在前一個案例的基礎上稍作做一下修改,只在父協程上新增 CoroutineExceptionHandler,照例上程式碼:

private fun testException(){
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} 處理異常 :$throwable")
    }
    GlobalScope.launch(CoroutineName("父協程") + exceptionHandler){
        val job = launch(CoroutineName("子協程")) {
            Log.d("${Thread.currentThread().name}","我要開始拋異常了" )            for (index in 0..10){
            launch(CoroutineName("孫子協程$index")) {
                Log.d("${Thread.currentThread().name}","${coroutineContext[CoroutineName]}" )
            }
        }
            throw NullPointerException("空指標異常")
        }        for (index in 0..10){
            launch(CoroutineName("子協程$index")) {
                Log.d("${Thread.currentThread().name}","${coroutineContext[CoroutineName]}" )
            }
        }
        try {
            job.join()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        Log.d("${Thread.currentThread().name}", "end")
    }
}
複製程式碼
D/DefaultDispatcher-worker-3: 我要開始拋異常了
W/System.err: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine is cancelling; job=StandaloneCoroutine{Cancelling}@f6b7807
W/System.err: Caused by: java.lang.NullPointerException: 空指標異常
W/System.err:     at com.carman.kotlin.coroutine.MainActivity$testException$1$job$1.invokeSuspend(MainActivity.kt:26//省略...
D/DefaultDispatcher-worker-6: end
D/exceptionHandler: CoroutineName(父協程) 處理異常 :java.lang.NullPointerException: 空指標異常
複製程式碼

我們看到子協程 job的異常被父協程處理了,無論我下面開啟多少個子協程產生異常,最終都是被父協程處理。但是有個問題是: 因為異常會導致父協程被取消執行,同時導致後續的所有子協程都沒有執行完成(可能偶爾有個別會執行完)。那可能就會是有人問了,這種做法的意義和應用場景是什麼呢?

Android版kotlin協程入門(三):kotlin協程的異常處理

如果有一個頁面,它最終展示的資料,是透過請求多個伺服器介面的資料拼接而成的,而其中某一個介面出問題都將不進行資料展示,而是提示載入失敗。那麼你就可以使用上面的方案去做,都不用管它們是誰報的錯,反正都是統一處理,一勞永逸。類似這樣的例子我們在開發中應該經常遇到。

Android版kotlin協程入門(三):kotlin協程的異常處理

但是另外一個問題就來了。例如我們APP的首頁,首頁上展示的資料五花八門。如:廣告,彈窗,未讀狀態,列表資料等等都在首頁存在,但是他們相互之間互不干擾又不關聯,即使其中某一個失敗了也不影響其他資料展示。那透過上面的方案,我們就沒辦法處理。

這個時候我們就可以透過 主從(監督)作用域的方式去實現,與 協同作用域一致,區別在於該作用域下的協程取消操作的單向傳播性,子協程的異常不會導致其它子協程取消。我再盜個官方圖:

Android版kotlin協程入門(三):kotlin協程的異常處理

我們在講解 主從(監督)作用域的時候提到過,要實現 主從(監督)作用域需要使用 supervisorScope或者 SupervisorJob。這裡我們需要補充一下,我們在使用 supervisorScope其實用的就是 SupervisorJob。 這也是為什麼使用 supervisorScope與使用 SupervisorJob協程處理是一樣的效果。

/**
 *  省略...
 * but overrides context's [Job] with [SupervisorJob].
 * 省略...
 */public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {   //省略...}
複製程式碼

這段是摘自官方文件的,其他的我把它們省略了,只留了一句:" SupervisorJob會覆蓋上下文中的 Job"。這也就說明我們在使用 supervisorScope的就是使用的 SupervisorJob。我們先用 supervisorScope實現以下我們上面提到的案例:

private fun testException(){
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d("exceptionHandler", "${coroutineContext[CoroutineName].toString()} 處理異常 :$throwable")
    }
    GlobalScope.launch(exceptionHandler) {
        supervisorScope {
            launch(CoroutineName("異常子協程")) {
                Log.d("${Thread.currentThread().name}", "我要開始拋異常了")
                throw NullPointerException("空指標異常")
            }            for (index in 0..10) {
                launch(CoroutineName("子協程$index")) {
                    Log.d("${Thread.currentThread().name}正常執行", "$index")                    if (index %3 == 0){
                        throw NullPointerException("子協程${index}空指標異常")
                    }
                }
            }
        }
    }
}
複製程式碼
D/DefaultDispatcher-worker-1: 我要開始拋異常了
D/exceptionHandler: CoroutineName(異常子協程) 處理異常 :java.lang.NullPointerException: 空指標異常
D/DefaultDispatcher-worker-1正常執行: 1
D/DefaultDispatcher-worker-1正常執行: 2
D/DefaultDispatcher-worker-3正常執行: 0
D/DefaultDispatcher-worker-1正常執行: 3
D/exceptionHandler: CoroutineName(子協程0) 處理異常 :java.lang.NullPointerException: 子協程0空指標異常
D/exceptionHandler: CoroutineName(子協程3) 處理異常 :java.lang.NullPointerException: 子協程3空指標異常
D/DefaultDispatcher-worker-4正常執行: 4
D/DefaultDispatcher-worker-4正常執行: 5
D/DefaultDispatcher-worker-5正常執行: 7
D/DefaultDispatcher-worker-3正常執行: 6
D/DefaultDispatcher-worker-5正常執行: 8
D/DefaultDispatcher-worker-5正常執行: 9
D/exceptionHandler: CoroutineName(子協程9) 處理異常 :java.lang.NullPointerException: 子協程9空指標異常
D/exceptionHandler: CoroutineName(子協程6) 處理異常 :java.lang.NullPointerException: 子協程6空指標異常
D/DefaultDispatcher-worker-2正常執行: 10
複製程式碼

可以看到即使當中有多個協程都出現問題,我們還是能夠讓所有的子協程執行完成。這個時候我們用這樣方案是不是就可以解決,我們首頁多種資料互不干擾的重新整理問題了,同也能夠在出現異常的時候統一處理。

那我們在用 SupervisorJob實現一遍,看看是不是和 supervisorScope一樣的,程式碼奉上:

private fun testException(){
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d("exceptionHandler", "${coroutineContext[CoroutineName].toString()} 處理異常 :$throwable")
    }
    val supervisorScope = CoroutineScope(SupervisorJob() + exceptionHandler)
    with(supervisorScope) {
        launch(CoroutineName("異常子協程")) {
            Log.d("${Thread.currentThread().name}", "我要開始拋異常了")
            throw NullPointerException("空指標異常")
        }        for (index in 0..10) {
            launch(CoroutineName("子協程$index")) {
                Log.d("${Thread.currentThread().name}正常執行", "$index")                if (index % 3 == 0) {
                    throw NullPointerException("子協程${index}空指標異常")
                }
            }
        }
    }
}
複製程式碼

可以看到我們透過 CoroutineScope建立一個 SupervisorJobsupervisorScope,然後再透過 with(supervisorScope)是不是就變得跟直接使用 supervisorScope一樣了。

D/DefaultDispatcher-worker-1: 我要開始拋異常了
D/DefaultDispatcher-worker-2正常執行: 0
D/exceptionHandler: CoroutineName(子協程0) 處理異常 :java.lang.NullPointerException: 子協程0空指標異常
D/exceptionHandler: CoroutineName(異常子協程) 處理異常 :java.lang.NullPointerException: 空指標異常
D/DefaultDispatcher-worker-2正常執行: 1
D/DefaultDispatcher-worker-2正常執行: 2
D/DefaultDispatcher-worker-4正常執行: 3
D/exceptionHandler: CoroutineName(子協程3) 處理異常 :java.lang.NullPointerException: 子協程3空指標異常
D/DefaultDispatcher-worker-1正常執行: 4
D/DefaultDispatcher-worker-4正常執行: 5
D/DefaultDispatcher-worker-4正常執行: 6
D/exceptionHandler: CoroutineName(子協程6) 處理異常 :java.lang.NullPointerException: 子協程6空指標異常
D/DefaultDispatcher-worker-4正常執行: 8
D/DefaultDispatcher-worker-3正常執行: 7
D/DefaultDispatcher-worker-2正常執行: 9
D/exceptionHandler: CoroutineName(子協程9) 處理異常 :java.lang.NullPointerException: 子協程9空指標異常
D/DefaultDispatcher-worker-3正常執行: 10
複製程式碼

當然,我們在使用協程的時候,可能某個協程需要自己處理自己的異常,這個時候只需要在這個協程的上下文中新增 CoroutineExceptionHandler即可。畢竟按需使用,誰也不知道產品又會有什麼奇怪的想法。

Android版kotlin協程入門(三):kotlin協程的異常處理

好了,到現在我們也基本的知道協程中的異常產生流程,和按需處理協程中的異常問題。如果您還有什麼不清楚的地方,可以自己動手實驗一下或者在下方留言、私信筆者等方式,我會在看到訊息的第一時間處理。

預告以及意見收集

在下一章節中,我們將會進入到實際的Android開發中,我們會先構建一個基礎APP的框架,封裝一些常用的協程方法和請求方式,至於具體的實戰專案型別,我想徵求一下大家的意見,然後根據反饋的實際情況再來決定,歡迎大家踴躍的提出意見。

最後:祝願大家都能寫出完美的BUG,讓測試都無法找到BUG所在。

作者:一個被攝影耽誤的程式猿
連結:
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2795401/,如需轉載,請註明出處,否則將追究法律責任。

相關文章