Android版kotlin協程入門(三):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
執行了
DispatchedTask
的
run
方法,最後
DispatchedTask.run
呼叫了
continuation
的
resumeWith
方法,
resumeWith
方法中在執行
invokeSuspend
的時候丟擲了異常。
再來個通熟一點的,你們應該就能猜出大概意思來。僱主先是找包工頭
CoroutineScheduler
要了一個工人
Worker
,然後給這個工人安排了一個搬磚任務
DispatchedTask
,同時告訴這個工人他要安全
runSafely
的搬磚,然後僱主就讓工人
Worker
開始工作
runWorker
,工人
Worker
就開始執行
executeTask
僱主吩咐的任務
DispatchedTask
,最後透過
resumeWith
來執行
invokeSuspend
的時候告訴僱主出現了問題(丟擲了異常).
彆著急,仔細想一想,有沒有發現這個跟
ThreadPoolExecutor
執行緒池和
Thread
執行緒的執行很像。包工頭就像是
ThreadPoolExecutor
執行緒池,工人就是
Thread
執行緒。
我們透過執行緒池(
CoroutineScheduler
)建立了一個
Thread
執行緒(
Worker
),然後開始執行執行緒(
runWorker
),執行緒裡面透過
executeTask
執行一個任務
DispatchedTask
,在執行任務的時候我們透過
try..catch
來保證任務安全執行
runSafely
,然後在
DispatchedTask
執行任務的時候,因為執行出現異常,所以在
catch
中透過
resumeWith
來告知結果執行緒出問題了。咦,邏輯好像突然變得清晰很多。
這麼看的話,這個協程異常的產生是不是基本原理就出來了。那麼我們接下里看看是不是正如我們所想的,我們先找到
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
繼承
Executor
,
Worker
繼承
Thread
,同時
runWorker
也是執行緒的
run
方法。在
runWorker
執行了
executeTask(task)
,接著在
executeTask
呼叫中
runSafely(task)
,然後我們看到
runSafely
使用
try..catch
了這個
task
任務的執行,最後在
catch
中丟擲了未捕獲的異常。那麼很明顯這個task肯定就是我們的
DispatchedTask
,那就到這裡結束了麼
很明顯並沒有,我們看到
catch
中丟擲的是個執行緒的
uncaughtExceptionHandler
,這個我們就很熟了,在Android開發中都是透過這個崩潰資訊。但是這個明顯不是我們這次的目標。
繼續往下分析,我們看看這個
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()) } } } 複製程式碼
接著我們繼續看
DispatchedTask
的
run
方法,前面怎麼獲取
exception
的我們先不管,直接看當
exception
不為空時,透過
continuation
的
resumeWithException
返回了異常。我們在上面提到過
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
啊,你是不是說錯了。
是滴,這裡只是一種可能,我們現在回到呼叫
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
庫的異常,我們這裡大致的瞭解下就行了。主要分為兩種:
-
kotlinx.coroutines
庫或編譯器有錯誤,導致的內部錯誤問題。 -
ThreadContextElement
也就是協程上下文錯誤,這是因為我們提供了不正確的ThreadContextElement
實現,導致協程處於不一致狀態。
public interface ThreadContextElement<S> : CoroutineContext.Element { public fun updateThreadContext(context: CoroutineContext): S public fun restoreThreadContext(context: CoroutineContext, oldState: S) } 複製程式碼
我們看到
handleFatalException
實際是呼叫了
handleCoroutineException
方法。
handleCoroutineException
是
kotlinx.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) } 複製程式碼
不知道各位是否理解了上面的流程,筆者最開始的時候也是被這裡來來回回的。繞著暈乎乎的。如果沒看懂的話,可以休息一下,揉揉眼睛,倒杯熱水,再回過頭捋一捋。
好滴,到此處為止。我們已經大概的瞭解kotlin協程中異常是如何丟擲的,下面我們就不再不過多延伸。下面我們來說說異常的處理。
協程的異常處理
kotlin協程異常處理我們要分成兩部分來看,透過上面的分解我們知道一種異常是透過
resumeWithException
丟擲的,還有一種異常是直接透過
CoroutineExceptionHandler
丟擲,那麼我們現在就開始講講如何處理異常。
第一種:當然就是我們最常用的
try..catch
啦,只要有異常崩潰我就先
try..catch
下,先不管流程對不對,我先保住我的程式不能崩潰。
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) 複製程式碼
當你以為使用
try..catch
就能捕獲的時候,然而實際並沒有。這是因為我們的
try..catch
使用方式不對,我們必須在使用
a[1]
時候再用
try..catch
捕獲才行。那就有人會想那我每次都記得使用
try..catch
就好了。
是,當然沒問題。但是你能保證你每次都能記住嗎,你的同一戰壕裡的戰友會記住嗎。而且當你的邏輯比較複雜的時候,你使用那麼多
try..catch
你程式碼閱讀性是不是降低了很多後,你還能記住哪裡有可能會出現異常嗎。
這個時候就需要使用協程上下文中的
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
呢? 這個時候我們就看出來,各位是否真的有吸收前面講解的知識:
-
第一種:我們上面講解的
協程作用域
部分你已經消化吸收,那麼恭喜你接下來的你可以大概的過一遍或者選擇跳過了。因為接下來的部分和協程作用域
中說到的內容大體一致。 -
第二種:除第一種的,都是第二種。那你接下來你就得認證仔細的看了。
我們之前在講到
協同作用域
和
主從(監督)作用域
的時候提到過,異常傳遞的問題。我們先來看看
協同作用域
:
-
協同作用域
如果子協程丟擲未捕獲的異常時,會將異常傳遞給父協程處理,如果父協程被取消,則所有子協程同時也會被取消。
容我盜個官方圖
預設情況下,當協程因出現異常失敗時,它會將異常傳播到它的父級,父級會取消其餘的子協程,同時取消自身的執行。最後將異常在傳播給它的父級。當異常到達當前層次結構的根,在當前協程作用域啟動的所有協程都將被取消。
我們在前一個案例的基礎上稍作做一下修改,只在父協程上新增
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
的異常被父協程處理了,無論我下面開啟多少個子協程產生異常,最終都是被父協程處理。但是有個問題是:
因為異常會導致父協程被取消執行,同時導致後續的所有子協程都沒有執行完成(可能偶爾有個別會執行完)。那可能就會是有人問了,這種做法的意義和應用場景是什麼呢?
如果有一個頁面,它最終展示的資料,是透過請求多個伺服器介面的資料拼接而成的,而其中某一個介面出問題都將不進行資料展示,而是提示載入失敗。那麼你就可以使用上面的方案去做,都不用管它們是誰報的錯,反正都是統一處理,一勞永逸。類似這樣的例子我們在開發中應該經常遇到。
但是另外一個問題就來了。例如我們APP的首頁,首頁上展示的資料五花八門。如:廣告,彈窗,未讀狀態,列表資料等等都在首頁存在,但是他們相互之間互不干擾又不關聯,即使其中某一個失敗了也不影響其他資料展示。那透過上面的方案,我們就沒辦法處理。
這個時候我們就可以透過
主從(監督)作用域
的方式去實現,與
協同作用域
一致,區別在於該作用域下的協程取消操作的單向傳播性,子協程的異常不會導致其它子協程取消。我再盜個官方圖:
我們在講解
主從(監督)作用域
的時候提到過,要實現
主從(監督)作用域
需要使用
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
建立一個
SupervisorJob
的
supervisorScope
,然後再透過
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開發中,我們會先構建一個基礎APP的框架,封裝一些常用的協程方法和請求方式,至於具體的實戰專案型別,我想徵求一下大家的意見,然後根據反饋的實際情況再來決定,歡迎大家踴躍的提出意見。
最後:祝願大家都能寫出完美的BUG,讓測試都無法找到BUG所在。
作者:一個被攝影耽誤的程式猿
連結:
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2795401/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android版kotlin協程入門(四):kotlin協程開發實戰AndroidKotlin
- Android Kotlin協程入門AndroidKotlin
- Android入門教程 | Kotlin協程入門AndroidKotlin
- Android版 kotlin協程入門(二):kotlin協程的關鍵知識點初步講解AndroidKotlin
- Kotlin協程快速入門Kotlin
- Kotlin Coroutine(協程): 三、瞭解協程Kotlin
- Android Kotlin 協程初探AndroidKotlin
- Kotlin的協程Kotlin
- 在 Android 開發中使用 Kotlin 協程 (一) -- 初識 Kotlin 協程AndroidKotlin
- Kotlin Coroutine(協程): 二、初識協程Kotlin
- Kotlin(android)協程中文翻譯KotlinAndroid
- Kotlin 協程一 —— CoroutineKotlin
- Kotlin 之旅7 協程Kotlin
- Kotlin Coroutines(協程)講解Kotlin
- Kotlin Coroutine(協程)簡介Kotlin
- Kotlin協程快速進階Kotlin
- Kotlin 協程使用手冊Kotlin
- Vert.x Kotlin 協程Kotlin
- kotlin協程的掛起suspendKotlin
- 【譯】kotlin 協程官方文件(1)-協程基礎(Coroutine Basics)Kotlin
- Kotlin協程學習之路【一】Kotlin
- Kotlin Coroutine(協程) 基本知識Kotlin
- Kotlin coroutine之協程基礎Kotlin
- 扒一扒Kotlin協程的底褲Kotlin
- 揭開Kotlin協程的神秘面紗Kotlin
- Kotlin 1.4.0-RC協程除錯Kotlin除錯
- 【譯】使用kotlin協程提高app效能KotlinAPP
- [譯] Kotlin 協程高階使用技巧Kotlin
- 【譯】kotlin 協程官方文件(6)-通道(Channels)Kotlin
- 異常處理過程
- [譯] 使用 Kotlin 協程改進應用效能Kotlin
- kotlin中將回撥改寫為協程Kotlin
- 【譯】第一次走進 Android 中的 Kotlin 協程AndroidKotlin
- 【思貨】kotlin協程優雅的與Retrofit纏綿-kotlin DSL簡述Kotlin
- 用Kotlin的方式來處理網路異常Kotlin
- 資源混淆是如何影響到Kotlin協程的Kotlin
- 真香!Kotlin+MVVM+LiveData+協程 打造 Wanandroid!KotlinMVVMLiveDataNaNAndroid
- rxjava回撥地獄-kotlin協程來幫忙RxJavaKotlin