协程coroutine
一、基本概念
1.1、什么是协程
即cooperation routine,协程基于线程,它是轻量级线程
1、处理耗时任务,这种任务常常会阻塞主线程。 2、保证主线程安全,即确保安全地从主线程调用任何suspend函数。
协程让异步逻辑同步化,杜绝回调地狱。 协程最核心的点就是,函数或者一段程序能够被挂起,稍后再在挂起的位 置恢复。
1.2、协程与异步任务对比
示例:绘制xml如图所示,请求网络并展示数据
1.2.1、异步任务
app manifest中配置http网络安全
android:networkSecurityConfig="@xml/network_security_config"
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
在Gradle中配置retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
定义网络请求接口UserApi.kt
data class User(val name: String, val address: String)
val userServiceApi: UserServiceApi by lazy {
val retrofit = retrofit2.Retrofit.Builder()
.client(OkHttpClient.Builder().addInterceptor {
it.proceed(it.request()).apply {
Log.d("jason", "request:${code()}")
}
}.build())
.baseUrl("http://192.168.2.101:8080/kotlinstudyserver/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
retrofit.create(UserServiceApi::class.java)
}
interface UserServiceApi {
@GET("user")
fun loadUser(@Query("name") name: String) : Call<User>
@GET("user")
suspend fun getUser(@Query("name") name: String) : User
}
其中baseUrl为localhost,采用maven项目生成,返回json
{
"name": "jason",
"address": "California"
}
ManActivity中定义异步任务请求网络
class MainActivity01 : AppCompatActivity() {
@SuppressLint("StaticFieldLeak")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val nameTextView = findViewById<TextView>(R.id.nameTextView)
val submitButton = findViewById<Button>(R.id.submitButton).also {
it.setOnClickListener {
object : AsyncTask<Void, Void, User>() {
override fun doInBackground(vararg p0: Void?): User? {
return userServiceApi.loadUser("xxx").execute().body()
}
override fun onPostExecute(user: User?) {
nameTextView.text = user?.address
}
}.execute()
}
}
}
}
点击请求后取得数据并刷新页面
1.2.2、协程
Gradle中添加协程依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0-RC-native-mt'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0-RC-native-mt'
修改点击事件
GlobalScope:顶级协程
launch:协程启动器
Dispatchers.Main:指定协程主线程,默认非主线程
withContext:协程的任务调度器(耗时操作,会开启子协程)
it.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
val user = withContext(Dispatchers.IO) {
userServiceApi.getUser("xxx")
}
nameTextView.text = user?.address
}
}
点击请求后取得数据并刷新页面
其中retrofit对协程有优化,这里可以不用切换线程
val user = userServiceApi.getUser("xxx")
1.3、挂起与恢复
常规函数基础操作包括:invoke(或call)和return,协程新增了suspend和resume:
suspend一也称为挂起或暂停,用于暂停执行当前协程,并保存所有局部变量; resume一用于让已暂停的协程从其暂停处继续执行。
1.3.1、调用栈中的函数调用流程
在下面示例中,分析调用顺序
1、顶级协程调用suspend函数getUser(),getUser()入栈
2、getUser()调用get(),getUser出栈挂起,get()入栈执行
3、get()结束调用show(user),属于getUser函数中的普通函数,getUser()恢复入栈,show(user)也入栈
class MainActivity03 : AppCompatActivity() {
private var nameTextView: TextView? = null
@SuppressLint("StaticFieldLeak")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
nameTextView = findViewById<TextView>(R.id.nameTextView)
val submitButton = findViewById<Button>(R.id.submitButton).also {
it.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
getUser()
}
}
}
}
private suspend fun getUser() {
val user = get()
show(user)
}
private suspend fun get() = withContext(Dispatchers.IO) {
userServiceApi.getUser("xxx")
}
private fun show(user: User) {
nameTextView?.text = user.address
}
}
1.3.2、挂起函数
使用suspend关键字修饰的函数叫作挂起函数。 挂起函数只能在协程体内或其他挂起函数内调用。
1.3.3、挂起与阻塞的区别
在以下延迟执行的示例中:
1、挂起、阻塞都运行在主线程
2、挂起不影响按钮弹起,阻塞会影响按钮弹起,时间长了会ANR
findViewById<Button>(R.id.submitButton).also {
it.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
delay(2000)
nameTextView?.text =
nameTextView?.text.toString() + "\n${Thread.currentThread().name}:after delay."
}
}
}
findViewById<Button>(R.id.submitButton2).also {
it.setOnClickListener {
Thread.sleep(2000)
nameTextView?.text =
nameTextView?.text.toString() + "\n${Thread.currentThread().name}:after delay."
}
}
其中delay函数是suspend函数,源码如下
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
1.4、任务调度
1.4.1、基础设施层与业务框架层
Kotlin的协程实现分为两个层次:
val continuation = suspend {
5
}.createCoroutine(object : Continuation<Int> {
override val context: CoroutineContext = EmptyCoroutineContext
override fun resumeWith(result: Result<Int>) {
println("协程结束:$result")
}
})
continuation.resume(Unit)
1.4.2、调度器
所有协程必须在调度器中运行,即使它们在主线程上运行也是如此。
Dispatchers.Main、Dispatchers.IO、Dispatchers.Default区别如下:
1.4.3、任务泄漏
当某个协程任务丢失,无法追踪,会导致内存、CPU、磁盘等资源浪费,甚至发 送一个无用的网络请求,这种情况称为任务泄漏。 为了能够避免协程泄漏,Kotlin引入了结构化并发机制。
1.4.4、结构化并发
使用结构化并发可以做到:
1.4.5、CoroutineScope
定义协程必须指定其CoroutineScope,它会跟踪所有协程,同样它还可以取消由它所启动的所有协程。常用的相关API有:
-
GlobalScope,生命周期是process级别的,即使Activity或Fragment已经被销毁,协程仍然在执行。 -
MainScope,在Activity中使用,可以在onDestroy0中取消协程。 -
viewModelScope,只能在ViewModel中使用,绑定ViewModeli的生命周期。 -
lifecycleScope,只能在Activity、Fragment中使用,会绑定Activity和Fragmenti的生命周期。
1.4.6、协程取消
协程取消使用mainScope.cancel()
class MainActivity06 : AppCompatActivity() {
private var nameTextView: TextView? = null
private var mainScope = MainScope()
@SuppressLint("StaticFieldLeak")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
nameTextView = findViewById<TextView>(R.id.nameTextView)
findViewById<Button>(R.id.submitButton).also {
it.setOnClickListener {
mainScope.launch(Dispatchers.Main) {
try {
delay(2000)
nameTextView?.text =
nameTextView?.text.toString() + "\n${Thread.currentThread().name}:after delay."
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
}
取消后,协程作用域的任务同步取消,并且能catch如下错误
W/System.err: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@5bd0c6a
1.4.7、协程委托
CoroutineScope:接口
by:接口代理关键字
MainScope:协程工厂类示例
MainActivity使用了通过MainScope返回值对象,即CoroutineScope by MainScope实现了一个委托,当MainActivity为委托对象,这样就可以直接调用被委托对象的方法来声明协程的函数
class MainActivity06 : AppCompatActivity(), CoroutineScope by MainScope() {
private var nameTextView: TextView? = null
@SuppressLint("StaticFieldLeak")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
nameTextView = findViewById<TextView>(R.id.nameTextView)
findViewById<Button>(R.id.submitButton).also {
it.setOnClickListener {
launch(Dispatchers.Main) {
try {
delay(2000)
nameTextView?.text =
nameTextView?.text.toString() + "\n${Thread.currentThread().name}:after delay."
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
二、协程启动
launch与async构建器都用来启动新协程
2.1、launch
不等待协程执行完成。
返回一个Job并且不附带任何结果值。
2.2、async
等待协程执行完成返回结果
返回一个Deferred,Deferred也是一个Job,可以使用.await()在一个延期的值上得到它的最终结果。
为了方便测试,可以在test目录下新建kotlin测试类
runBlocking把主线程包装成一个协程,它会等待其协程体以及所有子协程结束
下面示例说明:async可以返回一个lambda表达式的执行结果,但是lanuch不行
@Test
fun `test coroutine builder`() = runBlocking {
val job1 = launch {
delay(200)
println("job1 finished.")
}
val job2 = async {
delay(200)
println("job2 finished.")
"job2 result"
}
println(job2.await())
}
三、协程等待
launch:用join等待
async:用await等待
@Test
fun `test coroutine join`() = runBlocking<Unit> {
val job1 = launch {
delay(100)
println("One")
}
job1.join()
val job2 = launch {
delay(50)
println("two")
}
val job3 = launch {
delay(40)
println("three")
}
}
@Test
fun `test global scope coroutine await`() = runBlocking<Unit> {
val job1 = async {
delay(100)
println("One")
}
job1.await()
val job2 = launch {
delay(50)
println("two")
}
val job3 = launch {
delay(40)
println("three")
}
}
四、组合并发
使用suspend函数会等待上一个任务执行再执行下一个,使用组合并发可以大大节省任务的执行时间
示例中measureTimeMillis记录任务执行时间
private suspend fun doOne(): Int {
delay(1000)
return 14
}
private suspend fun doTwo(): Int {
delay(1000)
return 25
}
@Test
fun `test sync`() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doOne()
val two = doTwo()
println("The result:${one + two}")
}
println("Completed in $time ms")
}
使用async组合并发提高效率
注意这里async { doOne() }.await()等价于上面的写法,总时间会是两秒
@Test
fun `test combine async`() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doOne() }
val two = async { doTwo() }
println("The result:${one.await() + two.await()}")
}
println("Completed in $time ms")
}
五、协程的启动模式
5.1、DEFAULT
协程创建后,立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态。
下面示例中,job取消cancel结束了协程的运行
@Test
fun `test start mode`() = runBlocking<Unit> {
val job = launch(start = CoroutineStart.DEFAULT) {
var i = 0
while (true) {
delay(100)
i++
}
println("finished.")
}
delay(1000)
job.cancel()
}
5.2、ATOMIC
协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消。
5.3、LAZY
只有协程被需要时,包括主动调用协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,那么该协程将直接进入异常结束状态。
@Test
fun `test start mode`() = runBlocking<Unit> {
val job = async (start = CoroutineStart.LAZY) {
29
}
job.start()
}
5.4、UNDISPATCHED
协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点。
如何通过Dispatchers.IO让协程执行在主线程中?
下面示例中打印是在主线程,因为当前函数调用栈runBlocking在主线程
@Test
fun `test start mode`() = runBlocking<Unit> {
val job = launch(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
println("thread:"+Thread.currentThread().name)
}
}
如果是DEFAULT,则会打印子线程
相比于其他3种启动模式,只有UNDISPATCHED是立即执行
六、作用域构建器
6.1、coroutineScope
下面示例中coroutineScope包裹两个子协程,会等待两个子协程结束,子协程报错会传递到coroutineScope
@Test
fun `test coroutine scope builder`() = runBlocking<Unit> {
coroutineScope {
val job1 = launch {
delay(400)
println("job1 finished.")
}
val job2 = launch {
delay(200)
println("job2 finished.")
throw IllegalArgumentException()
}
}
}
coroutineScope与runBlocking的区别:
runBlocking是常规函数,而coroutineScope是挂起函数。 它们看起来很类似,它们都会等待其协程体以及所有子协程结束。 主要区别在于runBlocking方法会阻塞当前线程来等待,而coroutineScope只是挂起,会释放底层线程用于其他用途。
6.2、supervisorScope
一个协程失败了,不会影响其他兄弟协程
下面示例中,job2异常,但是job1仍然执行完成并打印了
@Test
fun `test supervisor scope builder`() = runBlocking<Unit> {
supervisorScope {
val job1 = launch {
delay(400)
println("job1 finished.")
}
val job2 = launch {
delay(200)
println("job2 finished.")
throw IllegalArgumentException()
}
}
}
七、job对象的生命周期
对于每一个创建的协程(通过launch或者async),会返回一个Job实例,该实例是协程的唯一标示,并且负责管理协程的生命周期。
7.1、Job的生命周期
一个任务可以包含一系列状态: 新创建(New)、活跃(Active)、完成中(Completing)、已完成(Completed)、取消中(Cancelling)和已取消(Cancelled)。 虽然我们无法直接访问这些状态,但是我们可以访问Job的属性:isActive、isCancelled和isCompleted。 如果协程处于活跃状态,协程运行出错或者调用 job.cancel() 都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true)。 当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时 isCompleted = true。
7.2、协程的取消
@Test
fun `test scope cancel`() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
delay(1000)
println("job 1")
}
scope.launch {
delay(1000)
println("job 2")
}
delay(100)
scope.cancel()
delay(1000)
}
CoroutineScope与coroutineScope
-
两者开头大小写不一样 -
CoroutineScope:构建协程作用域,用于自己创建一个协程作用域对象 -
coroutineScope:是协程的作用域构建器函数(管理子协程),参数需要传入一个匿名CoroutineScope对象 CoroutineScope的一个函数方法
- 在里面都可以通过lanuch asynic 创建启动子协程 取消子协程
@Test
fun `test brother job`() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
delay(1000)
println("job 1")
}
val job2 = scope.launch {
delay(1000)
println("job 2")
}
delay(100)
job1.cancel()
delay(1000)
}
- 特性3:所有kotlinx.coroutines中的挂起函数(withContext、delay等)都是可取消的。
下面直接GlobalScope.launch运行的示例会不会打印?
不会。GlobalScope有自己的作用域,没有继承父协程的上下文,也就没有继承runBlocking的作用域,导致主线程结束了,子线程也结束。处理方式是job1.join()让主线程等待
@Test
fun `test CancellationException`() = runBlocking<Unit> {
val job1 = GlobalScope.launch {
delay(1000)
println("job 1")
}
}
下面示例中,先cancel取消再等待join,调用cancel会使job进入到cancelling状态
@Test
fun `test CancellationException`() = runBlocking<Unit> {
val job1 = GlobalScope.launch {
try {
delay(1000)
println("job 1")
} catch (e: Exception) {
e.printStackTrace()
}
}
delay(100)
job1.cancel()
job1.join()
}
也可以合起来写作
job1.cancelAndJoin()
- 特性4:协程通过抛出一个特殊的异常CancellationException来处理取消操作。
cancel可以通过CancellationException自定义异常
job1.cancel(CancellationException("取消"))
7.3、CPU密集型任务取消
下面示例中,每秒打印消息两次,其余时间做频繁的计算System.currentTimeMillis() >= nextPrintTime。
取消job时发现并未取消,原因是协程对计算数据做了保护,防止丢失
@Test
fun `test cancel cpu task by isActive`() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
处理方式
7.3.1、isActive
isActive是一个可以被使用在CoroutineScope中的扩展属性,检查Job是否处于活跃状态。
使用isActive对循环体做判断
while (i < 5 && isActive) {
}
7.3.2、ensureActive
ensureActive(),如果job处于非活跃状态,这个方法会立即抛出异常。
ensureActive也是调用的isActive来实现的
while(i < 5){
ensureActive()
}
7.3.3、yield
yield函数会检查所在协程的状态,如果已经取消,则抛出CancellationException予以响应。 此外,它还会尝试出让线程的执行权,给其他协程提供执行机会。
如果要处理的任务属于:
- CPU 密集型,
- 可能会耗尽线程池资源,
- 需要在不向线程池中添加更多线程的前提下允许线程处理其他任务,那么请使用 yield()。
@Test
fun `test cancel cpu task by yield`() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default){
var nextPrintTime = startTime
var i = 0
while(i < 5){
yield()
if(System.currentTimeMillis() >= nextPrintTime){
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
7.4、协程取消的副作用
在 finally 中释放资源
@Test
fun `test release resources`() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
println("job: I'm running finally")
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
use函数:该函数只能被实现了Closeable的对象使用,程序结束的时候会自动调用close方法,适合文件对象。
with函数是run的变体,他们的功能行为是一样的,但with的调用方式不同,调用with时需要值参作为其第一个参数传入。
@Test
fun `test use function`() = runBlocking {
val br = BufferedReader(FileReader("/Users/zhanglei/Desktop/test.txt"))
with(br) {
var line: String?
while (true) {
line = readLine() ?: break
println(line)
}
close()
}
}
use里面it才是BufferedReader
@Test
fun `test use function`() = runBlocking {
BufferedReader(FileReader("/Users/zhanglei/Desktop/test.txt")).use {
var line: String?
while (true) {
line = it.readLine() ?: break
println(line)
}
}
}
查看源码,use函数内部实现了close关闭对象
@Test
fun `test use function`() = runBlocking {
println(BufferedReader(FileReader("/Users/zhanglei/Desktop/test.txt")).readText())
}
7.5、不能取消的任务
处于取消中状态的协程不能够挂起(运行不能取消的代码)
@Test
fun `test cancel with NonCancellable`() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
当协程被取消后需要调用挂起函数,我们需要将清理任务的代码放置于 NonCancellable CoroutineContext 中。 这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
7.6、超时任务
很多情况下取消一个协程的理由是它有可能超时。
@Test
fun `test deal with timeout`() = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常
@Test
fun `test deal with timeout return null`() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done"
}
println("Result is $result")
}
八、协程上下文
CoroutineContext是一组用于定义协程行为的元素。它由如下几项构成: ●Job:控制协程的生命周期 ●CoroutineDispatcher:向合适的线程分发任务 ●CoroutineName:协程的名称,调试的时候很有用 ●CoroutineExceptionHandler:处理未被捕捉的异常
8.1、组合上下文中的元素
有时我们需要在协程上下文中定义多个元素。我们可以使用+操作符来实现。比如说,我们可以显式指定一个调度器来启动协程并且同时显式指定一个命名:
@Test
fun `test CoroutineContext`() = runBlocking<Unit> {
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
}
Dispatchers.Default:调度器
CoroutineName(“test”):协程名字
协程对+进行了运算符重载
8.2、协程上下文的继承
对于新创建的协程,它的CoroutineContext会包含一个全新的io实例,它会帮助我们控制协程的生命周期。而剩下的元素会从CoroutineContext的父类继承,该父类可能是另外一个协程或者创建该协程的CoroutineScope。
@Test
fun `test CoroutineContext extend`() = runBlocking<Unit> {
val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("test"))
val job = scope.launch {
println("${coroutineContext[Job]} ${Thread.currentThread().name}")
val result = async {
println("${coroutineContext[Job]} ${Thread.currentThread().name}")
"OK"
}.await()
}
job.join()
}
示例中:CoroutineScope声明协程作用域,通过launch启动协程(join等待协程),job在通过async启动子协程(await等待子协程)。最后输出结果协程名称都是test,只不过编号不一样
九、协程异常
9.1、异常的传播
协程构建器有两种形式:自动传播异常launch与actor),向用户暴露异常async与produce)当这些构建器用于创建一个根协程时(该协程不是另一个协程的子协程),前者这类构建器,异常会在它发生的第一时间被抛出,而后者则依赖用户来最终消费异常,例如通过await:或receive.。
9.1.1、根协程异常传播
对于launch启动的根协程,异常捕获在具体任务中
对于async启动的根协程,异常捕获在await中
@Test
fun `test exception propagation`() = runBlocking<Unit> {
val job = GlobalScope.launch {
try {
throw IndexOutOfBoundsException()
} catch (e: Exception) {
println("Caught IndexOutOfBoundsException")
}
}
job.join()
val deferred = GlobalScope.async {
println("async")
throw ArithmeticException()
}
try {
deferred.await()
} catch (e: Exception) {
println("Caught ArithmeticException")
}
}
9.1.2、非根协程异常传播
其他协程所创建的协程中,产生的异常总是会被传播
下面示例中:async启动的协程不需要await就抛出异常了,而async启动的根协程不会
@Test
fun `test exception propagation2`() = runBlocking<Unit> {
val scope = CoroutineScope(Job())
val job = scope.launch {
async {
throw IllegalArgumentException()
}
}
job.join()
}
9.2、异常传播特性
当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级。接下来,父级会进行下面几步操作:
-
取消它自己的子级 -
取消它自己 -
将异常传播并传递给它的父级
9.2.1、SupervisorJob
使用SupervisorJob时,一个子协程的运行失败不会影响到其他子协程。SupervisorJob不会传播异常给它的父级,它会让子协程自己处理异常。 这种需求常见于在作用域内定义作业的U川组件,如果任何一个U川的子作业执行失败了,它并不总是有必要取消整个U组件,但是如果U组件被销毁了,由于它的结果不再被需要了,它就有必要使所有的子作业执行失败。
示例中使用SupervisorJob(),job2不受job1异常的干扰,仍然在执行
如果换成Job(),则job2会随着job1的异常而终止
@Test
fun `test SupervisorJob`() = runBlocking<Unit> {
val supervisor = CoroutineScope(SupervisorJob())
val job1 = supervisor.launch {
delay(100)
println("child 1")
throw IllegalArgumentException()
}
val job2 = supervisor.launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("child 2 finished.")
}
}
joinAll(job1, job2)
}
如果要取消协程,则可以使用supervisor.cancel()
注意:放在join前面
@Test
fun `test SupervisorJob`() = runBlocking<Unit> {
val supervisor = CoroutineScope(SupervisorJob())
val job1 = supervisor.launch {
delay(100)
println("child 1")
throw IllegalArgumentException()
}
val job2 = supervisor.launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("child 2 finished.")
}
}
delay(200)
supervisor.cancel()
joinAll(job1, job2)
}
9.2.2、supervisorScope
当作业自身执行失败的时候,所有子作业将会被全部取消。
下面示例:作业1抛出异常了,作业2仍然能运行
@Test
fun `test supervisorScope`() = runBlocking<Unit> {
supervisorScope {
launch {
delay(100)
println("child 1")
throw IllegalArgumentException()
}
try {
delay(Long.MAX_VALUE)
} finally {
println("child 2 finished.")
}
}
}
但是作业自身执行异常(即根协程),子协程也停止
yield:此处出让执行权,让子协程去执行,否则则协程还没执行,根协程就结束了
@Test
fun `test supervisorScope2`() = runBlocking<Unit> {
supervisorScope {
val child = launch {
try {
println("The child is sleeping")
delay(Long.MAX_VALUE)
} finally {
println("The child is cancelled")
}
}
yield()
println("Throwing an exception from the scope")
throw AssertionError()
}
}
9.3、异常的捕获
使用CoroutineExceptionHandleri对协程的异常进行捕获。 以下的条件被满足时,异常就会被捕获:
-
时机:异常是被自动抛出异常的协程所抛出的(使用launch,而不是async时); -
位置:在CoroutineScopel的CoroutineContext中或在一个根协程(CoroutineScope或者supervisorScope的直接子协程)中。
下面示例:AssertionError异常捕获到了(满足时机条件),ArithmeticException没有捕获到
@Test
fun `test CoroutineExceptionHandler`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException()
}
job.join()
deferred.await()
}
下面示例:异常捕获到了(满足位置条件),子协程launch抛出异常给父协程launch,因此能捕获到
@Test
fun `test CoroutineExceptionHandler2`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(Job())
val job = scope.launch(handler) {
launch {
throw IllegalArgumentException()
}
}
job.join()
}
但是如果lanuch捕获在内部协程,则不能捕获,下面示例就捕获不到会导致crash
@Test
fun `test CoroutineExceptionHandler3`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(Job())
val job = scope.launch {
launch(handler) {
throw IllegalArgumentException()
}
}
job.join()
}
9.4、异常捕获阻止App闪退
假设app有个按钮,点击执行如下业务
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val handler = CoroutineExceptionHandler { _, exception ->
Log.d("ning", "Caught $exception")
}
findViewById<Button>(R.id.button).also {
it.setOnClickListener {
GlobalScope.launch {
Log.d("ning", "on Click.")
"abc".substring(10)
}
}
}
}
上面示例中,由于字符串处理错误,导致App闪退。
如果我们捕获这个异常,就可阻止App闪退
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val handler = CoroutineExceptionHandler { _, exception ->
Log.d("ning", "Caught $exception")
}
findViewById<Button>(R.id.button).also {
it.setOnClickListener {
GlobalScope.launch(handler) {
Log.d("ning", "on Click.")
"abc".substring(10)
}
}
}
}
9.5、Android中全局异常处理
全局异常处理器可以获取到所有协程未处理的未捕获异常,不过它并不能对异常进行捕获,虽然不能阻止程序崩溃,全局异常处理器在程序调试和异常上报等场景中仍然有非常大的用处。 我们需要在classpath下面创建META-INF/services目录,并在其中创建一个名为kotlinx.coroutines.CoroutineExceptionHandler的文件,文件内容就是我们的全局异常处理器的全类名。
我们创建异常处理文件kotlinx.coroutines.CoroutineExceptionHandler(名称是固定的),内容就是异常处理的路径
com.example.coroutinedemo.GlobalCoroutineExceptionHandler
异常处理文件
class GlobalCoroutineExceptionHandler : CoroutineExceptionHandler {
override val key = CoroutineExceptionHandler
override fun handleException(context: CoroutineContext, exception: Throwable) {
Log.d("test", "Unhandled Coroutine Exception: $exception")
}
}
然后添加点击崩溃事件
val submitButton = findViewById<Button>(R.id.submitButton).also {
it.setOnClickListener {
GlobalScope.launch {
Log.d("test", "on Click.")
"abc".substring(10)
}
}
}
这样运行我们就能全局捕获到没有处理的协程异常,作用是可以在程序崩溃之前统计信息
9.6、取消与异常
下面示例中:yield出让线程执行权,child.cancelAndJoin()字协程取消后,由于被静默处理所以父协程没有终止,仍然能够打印
@Test
fun `test cancel and exception`() = runBlocking<Unit> {
val job = launch {
val child = launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled.")
}
}
yield()
println("Cancelling child")
child.cancelAndJoin()
yield()
println("Parent is not cancelled")
}
job.join()
}
withContext:协程的任务调度器(耗时操作,会开启子协程)
下面示例中:由于第二个子协程先抛异常,第一个子协程也会被取消,由于finally不会再执行挂起函数,这里使用withContext开启一个子协程
@Test
fun `test cancel and exception2`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children terminate")
delay(100)
println("The first child finished its non cancellable block")
}
}
}
launch {
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
job.join()
}
9.7、异常聚合
当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。在第一个异常之后发生的所有其他异常,都将被绑定到第一个异常之上。
下面示例中:IOException最先执行,因此为第一个异常,后续异常都会追加到这里。
@Test
fun `test exception aggregation`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
throw ArithmeticException()
}
}
launch {
try {
delay(Long.MAX_VALUE)
} finally {
throw IndexOutOfBoundsException()
}
}
launch {
delay(100)
throw IOException()
}
}
job.join()
}
|