kotlinコルーチンasync awaitの異常なステッピングピットと例外処理の正しい姿勢

Kotlin を使用して非同期操作を実行すること、特に Jetpack のいくつかのコンポーネントと組み合わせることは誰もがよく知っていると思います。これにより、Android 開発で非同期タスクを作成するのが非常に便利になります。

ただし、コルーチンを使用する場合、例外処理を理解するのに比較的時間がかかると個人的に感じています。

async await を使用すると例外処理の落とし穴に足を踏み入れる

kotlin コルーチンの例外処理の公式ドキュメント
まずは公式の例を見てみましょう。

ここに画像の説明を挿入

サンプル コードでは、 awaitが呼び出されたときに、非同期対応コルーチンの例外がキャプチャされていることがわかります印刷結果を見ると確かにキャプチャされており、問題ありません。

このサンプルコードによれば、ViewModel に次のようなコードを記述するなど、自然に使用できます。

    fun testAsync() {
    
    
        viewModelScope.launch {
    
    
            val deferred = async {
    
    
                LogUtils.e("准备抛出异常")
                delay(1000)
                throw Exception("async 抛出了一个异常")
            }
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")
        }
    }

公式サンプルコードによると、非同期例外は await が呼び出された場合にのみスローされます。そうすれば、await を呼び出すときに async によってスローされた例外をキャッチできるようになり、プログラムはクラッシュしなくなります。
実際に動作を見てみましょう:

ここに画像の説明を挿入

ここで、例外処理を再度開始するときに上記のコードを呼び出します。すると、アプリが実際にクラッシュしたことがわかります。印刷されたログを見てみましょう
ここに画像の説明を挿入

待機中に例外をキャッチしたことがわかりますが、それでもアプリがクラッシュするのはなぜでしょうか?

公式サンプルのコメントを注意深く見ると、ルート コルーチンという 2 つの単語が見つかります。
ここに画像の説明を挿入

結果を直接言います:

async がルート コルーチンとして使用されている場合、 await が呼び出されたときに遅延オブジェクトにカプセル化された例外がスローされます。
async がサブコルーチンとして使用されている場合、 await が呼び出されたときに例外はスローされませんが、すぐにスローされます

これが、await の代わりに try catch を明確に実行しているにもかかわらず、プログラムがクラッシュする理由です。
すぐにスローされた例外が処理されなかったため、クラッシュしただけです。

ただ、公式ドキュメントではサブコルーチンとして何が異常なのかが明確に説明されていないので、以前にも落とし穴を踏んでしまいました。

ここで疑問に思うかもしれません。いや、ログを見てみると、待機中に例外がスローされたことは明らかですが、例外がすぐにスローされたとどうやって言えるのか、そしてそれをどのように証明するのか、という疑問があるかもしれません。
ただ言えるのは、
ここに画像の説明を挿入
非常に簡単です。await の前に遅延を追加するだけです。ログ出力を確認するだけです。
コードは以下の通りで、上記と同様に直接遅延を加えています。

    fun testAsync() {
    
    
        viewModelScope.launch {
    
    
            val deferred = async {
    
    
                LogUtils.e("准备抛出异常")
                delay(1000)
                throw Exception("async 抛出了一个异常")
            }
            /*加个延时 主要是验证异常是不是在await的时候抛出*/
            delay(2000)
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")
        }
    }

もう一度実行状況を見てみると、await の try catch のログが出力される機会がないことがわかります。上記のコードが間違っているため、プログラムがクラッシュし、実行を続行するために 2 秒待つ機会がありません。上記のコードは非常に高速に実行されるため出力されるため、await 中にスローされた例外であるかのような錯覚を与えます。
ここに画像の説明を挿入

次に、それがトップレベルのスコープであるときに await を呼び出したときにスローされた例外であるかどうかを確認してみましょう。
この時点で async がトップレベルのスコープになることを除いて、同じ処理方法です。

    fun testTopAsync() {
    
    
        /*顶级作用域的async*/
        val deferred = viewModelScope.async {
    
    
            LogUtils.e("准备抛出异常")
            delay(1000)
            throw Exception("async 抛出了一个异常")
        }

        viewModelScope.launch {
    
    
            /*加个延时 主要是验证异常是不是在await的时候抛出*/
            delay(2000)        
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")

        }
    }

2 秒後の待機中に実際に例外がキャッチされ、アプリがクラッシュしていないことがわかります。

ここに画像の説明を挿入


kotlin コルーチンの例外処理の正しい姿勢

これが私が考える正しい対処法だと思うだけです

まず、コルーチンを使用する場合は必ずcoroutineExceptionhandlerを追加してください。

これは現在のコルーチンのスコープ内の例外をカバーするものです。つまり、スコープ内でキャッチされなかった例外は最終的に coroutineExceptionhandler に渡されて処理されるため、少なくともアプリはクラッシュしません。

次のコードを見てみましょう。

    /*异常处理*/
    private val exceptionHandler = CoroutineExceptionHandler {
    
     coroutineContext, throwable ->
        LogUtils.e("exceptionHandler:${
      
      throwable}")
    }

    fun testAsync() {
    
    
        viewModelScope.launch(exceptionHandler) {
    
    
            val deferred = async {
    
    
                LogUtils.e("准备抛出异常")
                delay(1000)
                throw Exception("async 抛出了一个异常")
            }
            /*加个延时 主要是验证异常是不是在await的时候抛出*/
            delay(2000)
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")
        }
    }

まだ前のコードですが、 CoroutineExceptionHandlerが追加されています。このCoroutineExceptionHandlerはそれほど冗長ではなく、公式Webサイトのドキュメントでより詳しく紹介されています。

次に、ランニング効果を見てみましょう。

ここに画像の説明を挿入


async によってスローされた未処理の例外が CoroutineExceptionHandler によって処理されることがわかります
。そのため、コルーチン ブロックに未処理の例外がスローされた場合でも、アプリがクラッシュすることはありません。

CoroutineExceptionHandler追加後のCoroutineScopeとsupervisorScopeの違いについては、ここでは紹介しませんが、公式サイトの方がわかりやすく紹介されています。

簡単に要約すると、次のようになります。

  • 各コルーチン内でCathを試してみるのが一番安全で、単純かつ乱暴ですが、面倒ですがミスも少なく安定しています。
  • try catch処理の有無に関わらず、念のためルートスコープにCoroutineExceptionHandlerを追加してください。
  • async await を使用する場合は、予期しない結果を避けるためにスコープの問題に注意してください。

さて、この記事はこれで終わりです、お役に立てば幸いです


この記事が役立つと思われる場合は、高評価をお願いします。より多くの開発者を助けることができます。記事に間違いがある場合は、修正してください。転載する場合は、Yu Zhiqiang のブログからの転載であることを明記してください。ありがとう_

おすすめ

転載: blog.csdn.net/yuzhiqiang_1993/article/details/121049744