協程 exception

不正常的Exception

除了CancellationException以外的Exception,都會讓協程拋出例外,讓子協程與父協程因例外被cancel(),不執行。

exception與cancel()

當子協程拋出exception,父協程收到exception,接下來會進行協程取消cancel()的動作。

  1. 取消子協程
  2. 取消它自己的協程
  3. 將exception,往上拋給自己的父親。

根協程 例外

launch

launch就直接拋出例外。
launch需要join()。

1
2
3
4
5
6
7
  @Test
  fun coroutin26() = runBlocking {
    val job = GlobalScope.launch {
      throw IndexOutOfBoundsException()
    }
    job.join()
  }

async

async需要透過傳回值await(),才可以補捉到例外。
async的await()本身就有join()的功能,會請runBlocking等待到job1執行完畢才退出。

1
2
3
4
5
6
7
8
9
10
11
@Test
fun coroutin27() = runBlocking {
  val job1 = GlobalScope.async {
    throw ArithmeticException()
  }
  try {
    job1.await()
  } catch (e: Exception) {
    e.printStackTrace()
  }
}

非根協程 協程中的協程例外

如果是協程中的協程產生例外,不用使用傳回值await(),直接拋出例外。

1
2
3
4
5
6
7
8
9
  @Test
  fun coroutin28() = runBlocking {
    val job = GlobalScope.launch {
      val child = async {
        throw IllegalArgumentException()
      }
    }
    job.join()
  }
Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.IllegalArgumentException

SupervisorJob() 與Job()

Job()

作用域的參數是Job()

CoroutineScope(Job())

子協程有例外,其它子協程也會被取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @Test
  fun coroutin01() = runBlocking {
    val scope = CoroutineScope(Job())
    val job1 = scope.launch {
      delay(100)
      throw IllegalArgumentException()
    }
    val job2 = scope.launch {
      delay(1000)
      println("job2 finish")
    }
    joinAll(job1, job2)
  }
}
java.lang.IllegalArgumentException

SupervisorJob()

作用域的參數是SupervisorJob()

CoroutineScope(SupervisorJob())

子協程有例外,其它子協程也會執行,以下執行結果,會顯示job2 finish。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @Test
  fun coroutin01() = runBlocking {
    val scope = CoroutineScope(SupervisorJob())
    val job1 = scope.launch {
      delay(100)
      throw IllegalArgumentException()
    }
    val job2 = scope.launch {
      delay(1000)
      println("job2 finish")
    }
    joinAll(job1, job2)
  }
}
job2 finish

java.lang.IllegalArgumentException

coroutineScope supervisorScope Exception

  • coroutinScope 子協程失敗,其它子協程全取消
  • supervisorScope 子協程失敗,不會影嚮其它子協程
  • supervisorScope 父協程失敗,其它子協程全取消

coroutineScope Exception

job2 拋出Exception(),導致job1被cancel(),不會執行job1 finish。

1
2
3
4
5
6
7
8
9
10
11
12
13
  fun coroutin06() = runBlocking {
    coroutineScope {
      val job1 = launch {
        delay(400)
        println("job1 finish")
      }
      val job2 = async{
        delay(200)
        println("job2 finish")
        throw IllegalArgumentException()
      }
    }
  }
job2 finish

java.lang.IllegalArgumentException
  at com.example.coroutine.Test01$coroutin06$1$1$job2$1.invokeSuspen

supervisorScope 子協程Exception

子協程job2 拋出Exception(),子協程job1仍會執行完畢。

1
2
3
4
5
6
7
8
9
10
11
12
13
  fun coroutin06() = runBlocking {
    supervisorScope {
      val job1 = launch {
        delay(400)
        println("job1 finish")
      }
      val job2 = async{
        delay(200)
        println("job2 finish")
        throw IllegalArgumentException()
      }
    }
  }
job2 finish
job1 finish

supervisorScope 父協程Exception

父協程失敗,其它子協程全取消。
child1、child2不會執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @Test
  fun coroutin02() = runBlocking {
    supervisorScope {
      val child1 = launch {
        println("child1")
      }
      val child2 = launch {
        println("child2")
      }
      // 這是父協程supervisorScope的Exception
      throw IllegalArgumentException()
    }
  }
}
java.lang.IllegalArgumentException

CoroutineExceptionHandler

只能補捉launch的Exception。

只能在supervisorScope、GlobalScope()、CoroutineScope()根協程下補捉作用域中所有Exception、包含作用域下子協程的例外。

GlobalScope

1
2
3
4
5
6
7
8
9
10
@Test
fun coroutin03() = runBlocking {
  val handler = CoroutineExceptionHandler{ _, exception ->
    println("exception = $exception")
  }
  val scope = GlobalScope.launch(handler){
    throw IllegalArgumentException()
  }
  scope.join()
}
exception = java.lang.IllegalArgumentException

CoroutineScope

1
2
3
4
5
6
7
8
9
10
@Test
fun coroutin04() = runBlocking {
  val handler = CoroutineExceptionHandler{ _, exception ->
    println("exception = $exception")
  }
  val scope = CoroutineScope(Job()).launch(handler){
    throw IllegalArgumentException()
  }
  scope.join()
}
exception = java.lang.IllegalArgumentException

handler補捉子協程例外

父協程的launch(handler),子協程有Exception也會補捉到Exception

CoroutineScope(Job()).launch(handler)
1
2
3
4
5
6
7
8
9
10
11
12
@Test
fun coroutin04() = runBlocking {
  val handler = CoroutineExceptionHandler{ _, exception ->
    println("exception = $exception")
  }
  val scope = CoroutineScope(Job()).launch(handler){
    val child = launch {
      throw IllegalArgumentException()
    }
  }
  scope.join()
}

handler無法補捉子協程例外

子協程launch(handler),handler無法補捉到Exception

val child = launch(handler) 
1
2
3
4
5
6
7
8
9
10
11
12
@Test
fun coroutin04() = runBlocking {
  val handler = CoroutineExceptionHandler{ _, exception ->
    println("exception = $exception")
  }
  val scope = CoroutineScope(Job()).launch{
    val child = launch(handler) {
      throw IllegalArgumentException()
    }
  }
  scope.join()
}

exception與finally

一個子協程拋出exception會造成其它子協程取消,但取消一定會執行finnaly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  @Test
  fun coroutin06() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
      println("exception = $exception")
    }
    val scope = CoroutineScope(Job()).launch(handler) {
      val child1 = launch {
        delay(500)
        throw IllegalArgumentException()
      }
      val child2 = launch {
        try {
          delay(2000)
          println("child2 finish")
        } finally {
          println("child2 finally")
        }
      }
      val child3 = launch {
        try {
          delay(2000)
          println("child3 finish")
        } finally {
          println("child3 finally")
        }
      }
    }
    scope.join()
  }
child2 finally
child3 finally
exception = java.lang.IllegalArgumentException

多個exception

當多個子協程exception,其它exception都會組合在第1個exception中。

但只能輸出第一個exception。

1
2
3
 val handler = CoroutineExceptionHandler { _, exception ->
   println("exception = $exception")
 }

其它exception,要特別使用以下語法處理。

1
${exception.suppressed.contentToString()}

以下程式碼補捉多個exception,child2、child3在finally拋出Exception,不管是否有取消,都會執行到,若拋出Exception沒寫在finally,會直接被cancel,就不會拋出exception。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 @Test
  fun coroutin06() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
      println("exception = $exception")
      println("other exceptions = ${exception.suppressed.contentToString()}")
    }
    val scope = CoroutineScope(Job()).launch(handler) {
      val child1 = launch {
        delay(500)
        throw IllegalArgumentException()
      }
      val child2 = launch {
        try {
          delay(2000)
          println("child2 finish")
        } finally {
          throw IndexOutOfBoundsException()
        }
      }
      val child3 = launch {
        try {
          delay(2000)
          println("child3 finish")
        } finally {
          throw ArithmeticException()
        }
      }
    }
    scope.join()
  }
exception = java.lang.IllegalArgumentException
other exceptions = [java.lang.ArithmeticException, java.lang.IndexOutOfBoundsException]

執行順序

以下程式碼會標註1,2,3,4,handler一定是最後執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 @Test
  fun coroutin06() = runBlocking {
    // 4.
    val handler = CoroutineExceptionHandler { _, exception ->
      println("exception = $exception")
      println("other exceptions = ${exception.suppressed.contentToString()}")
    }
    val scope = CoroutineScope(Job()).launch(handler) {
      // 1.
      val child1 = launch {
        delay(500)
        throw IllegalArgumentException()
      }
      val child2 = launch {
        try {
          delay(2000)
          println("child2 finish")
        } finally {
          // 2.
          println("child2 finally")
        }
      }
      val child3 = launch {
        try {
          delay(2000)
          println("child3 finish")
        } finally {
          // 3.
          throw ArithmeticException()
        }
      }
    }
    scope.join()
  }

Android建立全域ExceptionHandler

在main目錄下,建立resources目錄,根據以下階層關係,建立三個目錄。

main/resources/META-INF/services/

對著上面的目錄按滑鼠右鍵,建立檔案
img

檔名是kotlinx.coroutines.CoroutineExceptionHandler
img

內容為

com.example.coroutine.handler.GlobalExceptionHandler

自己寫一個GlobalExceptionHandler,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.coroutine.handler

import android.util.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlin.coroutines.CoroutineContext

class GlobalExceptionHandler : CoroutineExceptionHandler {
  override val key = CoroutineExceptionHandler
  override fun handleException(context: CoroutineContext, exception: Throwable) {
    Log.d("cici", "Global exception: $exception")
  }
}

試著執行以下程式碼,能否補捉到exception。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainActivity06 : AppCompatActivity() {
  val mainScope = MainScope()
  var textv: TextView? = null
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val submit = findViewById<Button>(R.id.button)
    textv = findViewById<TextView>(R.id.textView)
    submit.setOnClickListener {
      GlobalScope.launch {
        Log.d("cici", "click")
        "abc".substring(10)
      }
    }
  }
}
Global exception: java.lang.StringIndexOutOfBoundsException: length=3; index=10

Android建立區域ExceptionHandler

以下自訂handler,把handler作為參數傳入launch()協程構建器中。

GlobalScope.launch(handler)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity06 : AppCompatActivity() {
  val mainScope = MainScope()
  var textv: TextView? = null
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val submit = findViewById<Button>(R.id.button)
    val handler = CoroutineExceptionHandler {
      _, exception ->
      Log.d("cici", "activity exception = $exception")
    }
    textv = findViewById<TextView>(R.id.textView)
    submit.setOnClickListener {
      GlobalScope.launch(handler) {
        Log.d("cici", "click")
        "abc".substring(10)
      }
    }
  }
}
activity exception = java.lang.StringIndexOutOfBoundsException: length=3; index=10

results matching ""

    No results matching ""