协程:
前言:
一、协程难在哪儿?
1.Java中不曾出现的,新概念。
2.概念不清晰,我们看到的大都是不同语言对于协程的实现或者衍生。
3.Kotlin基础不扎实(匿名标准函数、代理、委托,各种设计模式,新语法新特性)。
4.多线程编程基础太薄弱。
二、协程是什么,在Android中协程解决什么问题?
1.协程基于线程,它是轻量级线程。
2.Android中处理耗时任务,这种任务常常会阻塞主线程。保证主线程安全,即确保安全地从主线程调用任何suspend函数。
三、Android11 Google官宣异步任务过时,建议使用协程替代异步任务。
coroutine
cooperation
routine
异步任务和协程的写法比较:
声明网络安全的生命:internet权限 + android:networkSecurityConfig
GuideLine
标准函数:
val submitButton = findViewById<Button>(R.id.submitButton).also{
it.setOnClickListener{
// 对象表达式
object: AsyncTask<Void,Void,User>() {
override fun doInBackground(vararg params: Void?): User? {
// 子线程
return userServiceApi.loadUser("xxx").execute().body()
}
override fun onPostExecute(user: User?) {
// 主线程
// xxx:textview组件
xxx.text = "address: ${user?.address}"
}
}.execute(); // 执行异步任务
}
}
api:
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("weip", "request: ${code()}")
}
}.build())
.baseUrl("http://192.168.1.4:8080/kotlinserver/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
retrofit.create(UserServiceApi::class.java)
}
interface UserServiceApi{
@GET("user")
fun loadUser(@Query("name") name: String) : Call<User>
@GET("user")
suspend getUser(@Query("name") name: String) : User
}
// 异步任务的缺陷: 阅读是反人性。 onPostExecute在doInBackground之前也可以写。跳跃性思维。回调地狱乱的问题。
人类的思维应该是陈述式方式,串行思维:
协程就是解决这样的问题。
GlobalScope.launch(Dispatchers.Main){
// 顶级协程。这块设计协程作用域的问题。 launch:协程构建器
val user = withContext(Dispatchers.IO) { // 任务调度器开启后台线程
userServiceApi.getUser("xxx")
}
xxx.text = "address: ${user?.address}"
}
相比较之前的写法要清爽很多。
协程是什么?
协程让异步逻辑同步化,杜绝回调地狱。
协程最核心的点就是,函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。
协程的挂起和恢复。
常规函数基础操作包括: invoke(或call)和return,协程新增了suspend和resume
suspend:也成为挂起活暂停,用于暂停执行当前协程,并保存所有局部变量;
resume:用于让已暂停的协程从其暂停处继续执行。
堆栈帧中的函数调用流程:
suspend fun getUser() {
val user = get()
show(user)
}
挂起函数:
使用suspend关键字修饰的函数叫作挂起函数。
挂起函数只能在协程体内活其它挂起函数内调用。
挂起和阻塞:
delay(12000) //挂起12s
Thread.sleep(12000) //阻塞12s
协程的基础设施层和业务框架层
协程的两部分:
Kotlin协程实现分为两个层次:
基础设施层,标准的协程API,主要是对协程提供了概念和语义上最基本的支持
业务框架层,协程的上层框架支持
NIO:非阻塞的IO 高并发多线程的API
Netty:NIO的一个框架
基础设施层:kotlin/
// 协程体
val continuation = suspend {
5
}.createCoroutine(object: Continuation<Int>{
override val context: CotoutineContext = EmptyCoroutiueContext
override fun resumeWith(result: Result<Int>) {
println("Coroutine End: $result")
}
})
continuation.resume(Unit)
业务框架层:kotlinx/
协程的调度器:
Dispatchers.Main
Dispatchers.IO(专为磁盘和网络IO进行了优化,如数据库,文件读写,网络处理)/Default(CPU密集型任务执行)
任务泄露:
当某个协程任务丢失,无法追踪,会导致内存,CPU,磁盘等资源浪费。甚至发送一个无用的网络请求,这种情况称为任务泄露。
为了能够避免协程泄露,Kotlin引入了结构化并发机制。
结构化并发:
使用结构化并发可以做到:
取消任务,当某项任务不再需要时取消它。
追踪任务,当任务正在执行时,追踪它。
发出错误信号,当协程失败时,发出错误信号表明有错误发生。
CoroutineScope:
定义协程必须执行其CoroutineScope,它会跟踪所有协程,同样它还可以取消由它所启动的所有协程。
常用的相关API有:
GlobalScope,生命周期是process级别的,即使Activity或Fragment已经被销毁,协程仍然在执行。
MainScope,在Activity中使用,可以在onDestroy()中取消协程。
ViewModelScope,只能在ViewModel中使用,绑定ViewModel的生命周期。
LifecycleScope,只能在Activity或Fragment中使用,会绑定Activity和Fragment的生命周期。
MainScope使用:
依赖的库有:
implemetation "androidx.core:core-ktx:+"
implemetation 'org.jetbrains.kotlin:hotlin-stdlib-jdk:$kitlin_version'
implemetation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0-RC-native-mt'
implemetation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0-RC-native-mt'
implemetation "androidx.activity:activity-ktx:1.1.0"
implemetation "androidx.lifecycle:liftcycle-runtime-ktx:2.2.0"
implemetation "androidx.lifecycle:liftcycle-viewmodel-ktx:2.2.0"
implemetation "androidx.lifecycle:liftcycle-livedata-core-ktx:2.2.0"
implemetation "androidx.lifecycle:liftcycle-livedata-ktx:2.2.0"
implemetation "com.squareup.retrofit2:retrofit:2.9.0"
implemetation "com.squareup.retrofit2:converter-moshi:2.9.0"
private val mainScope = MainScope() // 工厂设计模式
mainScope.launch{
val user = userServiceApi.getUser("xx") // retrofit看到你是挂起的函数,框架会自动启动它的协程的支持,会有异步的线程执行此逻辑
nameTextView?.text = "address:${user?.address}" // main主线程
// 验证协程取消 10s
try {
delay(10000)
} catch(e:Exception) {
e.printStackTrace();
}
}
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
// 换种写法
private val mainScope = MainScope()
class MainActivity: AppCompatActivity(), CoroutineScope by MainScope() {
// private val mainScope = MainScope() // 工厂设计模式
launch{
val user = userServiceApi.getUser("xx") // retrofit看到你是挂起的函数,框架会自动启动它的协程的支持,会有异步的线程执行此逻辑
nameTextView?.text = "address:${user?.address}" // main主线程
// 验证协程取消 10s
try {
delay(10000)
} catch(e:Exception) {
e.printStackTrace();
}
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
协程上手:
databing环境配置
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
dataBinding {
enabled = true
}
private val mainViewModel : MainViewModel by viewModels
val bingding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
binding.viewModel = mainViewModel
binding.lifecycleOwner = this
binding.submitButton.also{
it.setOnClickListener {
mainViewModel.getUser("xxx")
}
}
//优化
binding.submitButton.setOnClickListener {
mainViewModel.getUser("xxx")
}
class MainViewModel: ViewModel {
val userLiveData = MutableLiveData<User>
}
<layout>
<data>
<variable
name="viewModel"
type="....."
/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
// 注意:
buildscript {
ext.kotlin_version = '1.5.0'
}
app:
kotlinOptions {
jvmTarget = "1.8"
}
class UserRepository {
suspend fun getUser(name: String): User {
return userServiceApi.getUser(name)
}
}
class MainViewModel() : ViewModel() {
val userLiveData = MutableLiveData<User>()
private val userRepository = UserRepository()
fun getUser(name: String) {
viewModelScope.launch {
userLiveData.value = userRepository.getUser(name)
}
}
}
launch与async返回值比较:
launch与async构建器都用来启动新协程。
launch,返回一个Job并且不附加任何结果值。
async,返回一个Deferred,Deferred也是一个Job,可以使用.await()在一个延期的值上得到它的最终结果。
单元测试验证(kotlin 1.5.0版本上):
import org.junit.Test
class CoroutineTest01{
@Test
fun `test coroutine builder`() {
GloalScope.launch {
}
}
// 把主线程变成一个主协程,可以直接使用launch和async构建器 runBlocking会等待2个子协程执行完毕
@Test
fun `test coroutine builder01`() = runBlocking {
val job1 = launch{
delay(200)
println("job1 finished.")
}
val job2 = async{
delay(200)
println("job2 finished.")
"job2 result"
}
println(job2.await())
}
}
join与await等待协程的作业:
@Test
fun `test coroutine join`() = runBlocking {
val job1 = launch{
delay(10000) //10s
println("One")
}
job1.join()
val job2 = launch{
delay(200)
println("Two")
}
val job3 = launch{
delay(200)
println("Three")
}
}
@Test
fun `test coroutine await`() = runBlocking {
val job1 = async{
delay(10000) //10s
println("One")
}
job1.await()
val job2 = async{
delay(200)
println("Two")
}
val job3 = async{
delay(200)
println("Three")
}
}
async组合并发:
@Test
fun `test sync`() = runBlocking {
val time = measuteTimeMillis {
val one = doOne()
val two = doTwo()
println("The result:${one + two}")
}
println("Completed in $time ms") // 2022ms 2s多
}
// async结构化并发
@Test
fun `test combine async`() = runBlocking {
val time = measuteTimeMillis {
val one = async {doOne()}
val two = async {doTwo()}
println("The result:${one.await() + two.await()}") // 1022ms 1s多一点
}
println("Completed in $time ms") // 2s多
}
// 结构化并发错误写法
@Test
fun `test combine error async`() = runBlocking {
val time = measuteTimeMillis {
val one = async {doOne()}.await() // 等待这个做完
val two = async {doTwo()}.await() // 等待这个做完
println("The result:${one + two}") // 2022ms 2s多
}
println("Completed in $time ms") // 2s多
}
private suspend fun doOne(); Int {
delay(1000)
return 14
}
private suspend fun doTwo(); Int {
delay(1000) //1s
return 25
}
协程的启动模式:
DEFAULT:协程创建后,立即开始调度,在调度前如果协程被取消,其将直接进入取消相应的状态。
ATOMIC:协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消。
LAZY:只有协程被需要时,包括主动调用协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,那么该协程将直接进入异常结束状态。
UNDISPATCHED:协程创建后立即在当前函数调度栈中执行,真正遇到第一个真正挂起的点。
@Test
fun `test start mode`() = runBlocking {
val job = launch(start = CoroutineStart.ATIMIC) {
// ...
delay(10000)
println("Job finished")
}
delay(1000)
job.cacel()
val job1 = async(start = CoroutineStart.LAZY) {
29
}
// ......执行一些计算
job1.await()
// 面试题:如何在通过使用Dispatchers.IO,这个协程仍然在主线程里面执行代码 ===》UNDISPATCHED
// 协程创建后立即在当前函数调度栈中(主线程)执行 UNDISPATCHED:不转发
val job2 = async(context =Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
println("thread:" + Thread.currentThread().name) // main线程,如果是DEFAULT,则是子线程里
}
}
协程的作用域构建器:
coroutineScope与runBlocking
runBlocking是常规函数,而coroutineScope是挂起函数。
他们都会等待其协程体以及所有子协程结束,主要区别在于runBlocking方法会阻塞当前线程来等待,而coroutineScope只是挂起,会释放底层线程用于其他用途。
coroutineScope与supervisorScope
coroutineScope:一个协程失败了,所有其他兄弟协程也会被取消。
supervisorScope:一个协程失败了,不会影响其他兄弟协程。
@Test
fun `test coroutine scope builder`() = runBlocking {
coroutineScope {
val job1 = launch{
delay(400)
println("job1 finished.")
}
val job2 = async{
delay(200)
println("job2 finished.")
"job2 result"
throw IllegalArgumentException() // 抛出异常
}
}
}
@Test
fun `test supervisor scope builder`() = runBlocking {
supervisorScope {
val job1 = launch{
delay(400)
println("job1 finished.")
}
val job2 = async{
delay(200)
println("job2 finished.")
"job2 result"
throw IllegalArgumentException() // 抛出异常
}
}
}
Job对象的生命周期:
对于每一个创建的协程(通过launch或者async),会返回一个Job实例,该实例是协程的唯一标识,并且负责管理协程的生命周期。
一个任务可以包含一系列状态:新创建(New)、活跃(Active)、完成中(Completing)、已完成(Completed)、取消中(Cancelling)和已取消(Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问Job的属性:isActive、isCancelled和isCompleted。
如果协程处于活跃状态,协程运行出错或者调用job.cancel()都会将当前任务置为取消中(Cancelling)状态(isActive = false,isCancelled = true)。
当所有的子协程都完成后,协程会进入已取消(Cancelled)状态,此时isCompleted = true。
New---[start]---Active---[complete]---Completing---[finish]---Completed
Active/Completing---[cancel/fail]---Cancelling ---[finish]---Cancelled
取消作用域/取消兄弟协程/协程取消的异常:
取消作用域会取消它的子协程。
被取消的子协程并不会影响其余兄弟协程。
协程通过抛出一个特殊的异常 CancellationException 来处理取消操作。
所有kotlinx.coroutines中的挂起函数(withContext、delay等)都是可取消的。
@Test
fun `test scope builder`() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.Default)
scope.launch{
delay(1000)
println("job1")
}
scope.launch{
delay(1000)
println("job2")
}
delay(100)
scope.cancel()
delay(2000)
}
@Test
fun `test brother builder`() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch{
delay(1000)
println("job1")
}
val job2 = scope.launch{
delay(1000)
println("job2")
}
delay(100)
job1.cancel() // job1取消,job2仍然可以执行
delay(2000)
}
@Test
fun `test CancellationException`() = runBlocking<Unit> {
val job1 = GlobalScope.launch{
try{
delay(1000)
println("job1")
}catch(e:Exception) {
e.printStackTrace()
}
}
delay(100)
job1.cancel(CancellationException("取消"))
job1.join()
job1.cancelAndJoin() // 上面两步可以结合到一起来写
}
CPU密集型任务取消-isActive/ensureActive/yield:
isActive是一个可以被使用在CoroutineScope中的扩展属性,检查Job是否处于活跃状态。
ensureActive(),如果job处于非活跃状态,这个方法会立即抛出异常。
yield函数会检查所在协程的状态,如果已经取消,则抛出CancellationException予以响应。
此外,它还会尝试出让线程的执行权,给其他协程提供执行机会。
@Test
fun `test cacel cpu task by isActive`() = runBlocking<Unit> {
val startTime = System.currentTimeMillis();
var job = launch(Diapatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
if(System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500
}
}
}
delay(1300)
println("main: I'm tired of waiting!")
job.cancelAndJoin
println("main: Now I can quit.")
}
// 多一个isActive cpu密集型的任务就会中断
@Test
fun `test cacel cpu task by isActive2`() = runBlocking<Unit> {
val startTime = System.currentTimeMillis();
var job = launch(Diapatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5 && isActive) {
if(System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500
}
}
}
delay(1300)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
// ensureActive (异常被静默处理掉了)
@Test
fun `test cacel cpu task by ensureActive`() = runBlocking<Unit> {
val startTime = System.currentTimeMillis();
var job = launch(Diapatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5 ) {
ensureActive()
if(System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500
}
}
}
delay(1300)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
// yield (异常被静默处理掉了)
@Test
fun `test cacel cpu task by yield`() = runBlocking<Unit> {
val startTime = System.currentTimeMillis();
var job = launch(Diapatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5 ) {
yield()
if(System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500
}
}
}
delay(1300)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
协程取消的副作用:
在finally中释放资源。
use函数:该函数只能被实现了Closeable的对象使用,程序结束的时候会自动调用close方法,适合文件对象。
@Test
fun `test release resoures`() = runBlocking<Unit> {
var job = launch(Diapatchers.Default) {
try{
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L) // 500ms
}
}finally{
println("main: I'm running finally")
}
}
delay(1300)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
@Test
fun `test use function`() = runBlocking<Unit> {
var br = BufferedReader(FileReader("D:\\I have a dream.txt"))
with(br) {
var line: String?
try{
while(true) {
line = readLine() ?: break;
println(line)
}
}finally{
close()
}
}
BufferedReader(FileReader("D:\\I have a dream.txt")).use{
var line: String?
while(true) {
line = it.readLine() ?: break;
println(line)
}
}
}
标准函数use(自动关闭文件对象):
BufferedReader(FileReader("D:\\I have a dream.txt")).use{
var line: String?
while(true) {
line = it.readLine() ?: break;
println(line)
}
}
不能被取消的任务:
处于取消中状态的协程不能够挂起(运行不能取消的代码),当协程被取消后需要调用挂起函数,我们需要将清理任务的代码放置于NonCancellable CoroutineContext中。
这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。
@Test
fun `test cancel with NonCancellable`() = runBlocking<Unit> {
var job = launch(Diapatchers.Default) {
try{
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L) // 500ms
}
}finally{
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");
}
}
}
delay(1300)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
超时任务:
很多情况下取消一个协程的理由是它有可能超时。
withTimeoutOrNull 通过返回null来进行超时操作,从而替代抛出一个异常。
@Test
fun `test deal with timeout`() = runBlocking<Unit> {
withTimeout(1300) { // 1300ms执行完下面的业务
repeat(1000) { i -> // 1000次这样的操作
println("job: I'm sleeping $i ...")
delay(500L)
}
}
}
@Test
fun `test deal with timeout return null`() = runBlocking<Unit> {
val result = withTimeoutOrNull(1300) { // 1300ms执行完下面的业务
repeat(1000) { i -> // 1000次这样的操作
println("job: I'm sleeping $i ...")
delay(500L)
}
"Done"
} ?: "Jack"
println("Result is $result")
}
==================协程的异常处理
什么是协程的上下文:
CoroutineContext是一组定义协程行为的元素。它由如下几项构成:
Job:控制协程的生命周期
CoroutineDispatcher:向合适的线程分发任务
CoroutineName:协程的名称,调试的时候很有用
CoroutineExceptionHandler:处理未被捕捉的异常。
组合协程上下文的元素:
有时我们需要在协程上下文中定义多个元素。我们可以使用+操作符来实现。比如说,我们可以显式指定一个调度器来启动协程并且同时显示指定一个命名:
@Test
fun `test CoroutineContext`() = runBlocking<Unit> {
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread()name}")
}
}
协程上下文的继承/继承公式:
对于新创建的协程,它的CoroutineContext会包含一个全新的Job实例,它会帮助我们控制协程的生命周期。而剩下的元素会从CoroutineContext的父类继承,该父类可能是另外一个协程或者创建该协程的CoroutineScope。
@Test
fun `test CoroutineContext extend`() = runBlocking<Unit> {
val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("test"))
val job = scope.launch{
// 新的协程会将CoroutineScope作为父级
println("{coroutineContext\[Job\] {Thread.currentThread()name}")
val result = async {
// 通过async创建的新协程会将当前协程作为父级
println("{coroutineContext\[Job\] {Thread.currentThread()name}")
"OK"
}.await()
}
job.join()
}
协程的上下文 = 默认值 + 继承的CoroutineContext + 参数
一些元素包含默认值: Dispatchers.Default是默认的 CoroutineDispatcher,以及"coroutine"作为默认的CoroutineName;
继承的CoroutineContext是CoroutineScope或者其父协程的CoroutineContext;
传入协程构建器的参数的优先级高于继承的上下文参数,因此会覆盖对应的参数值。
@Test
fun `test CoroutineContext extend2`() = runBlocking<Unit> {
val coroutineExceptionHandler = CoroutineExceptionHandler{ -, Exception -> {
println("Caught $Exception");
}}
val scope = CoroutineScope(Job() + Dispatchers.Main + coroutineExceptionHandler)
val job = scope.launch(Dispatchers.IO) {
// 新协程
}
}
最终的父级CoroutineContext会内含Dispatchers.IO而不是scope对象里的Dispatchers.Main,因为它被协程的构建器里的参数覆盖了。
此外注意以下父级CoroutineContext里的Job是scope对象的Job,而新的Job实例会赋值给新的协程的CoroutineContext。
学习协程异常处理的必要性:
当应用出现一些意外情况时,给用户提供合适的体验非常重要,一方面,目睹应用崩溃是个很糟糕的体验,另一方面,在用户操作失败时,也必须要能给出正确的提示信息。
自动传播异常与向用户暴露异常:
协程构建器有两种形式:自动传播异常(launch与actor),向用户暴露异常(async与produce)当这些构建器用于创建一个根协程时(该协程不是另一个协程的子协程),
前者这类构建器,异常会在它发生的第一时间被抛出,而后者则依赖用户来最终消费异常,例如通过await或receive。
@Test
fun `test exception propagation`() = runBlocking<Unit> {
// 根协程
val job = GlobalScope.launch {
try{
throw IndexOutOfBoundsException()
}catch(e:Excption){
println("Caught IndexOutOfBoundsException")
}
}
job.join()
val deferred = GlobalScope.async {
throw ArithmeticException()
}
try{
deferred.await()
}catch(e:Excption){
println("Caught ArithmeticException")
}
}
非根协程的异常传播:
其他协程所创建的协程中,产生的异常总是会被传播。
@Test
fun `test exception propagation2`() = runBlocking<Unit> {
val scope = CoroutineScope(Job())
val job = scope.launch {
async{
throw IllegalArgumentException()
//如果async抛出异常,launch就会立即抛出异常,而不会调用.await()
}
}
job.join()
}
异常的传播特性:
当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级。接下来父级会进行下面几步操作:
取消它自己的子级。
取消它自己。
将异常传播并传递给它的父级。
SupervisorJob:
使用SupervisorJob时,一个子协程的运行失败不会影响到其他子协程。
SupervisorJob不会传播异常给它的父级,它会让子协程自己处理异常。
这种需求常见于在作用域内定义作业的UI组件,如果任何一个UI的子作业执行失败了,它并不总是有必要取消整个UI组件,但是如果UI组件被销毁了,由于它的结果不再被需要了,它就有必要使所有的子作业执行失败。
@Test
fun `test supervisorJob`() = runBlocking<Unit> {
// SupervisorJob改成Job()再试试效果
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)
}
supervisorScope:
当作业自身执行失败的时候,所有子作业将会被全部取消。
@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")
}
}
}
@Test
fun `test supervisorScope2`() = runBlocking<Unit> {
try{
supervisorScope{
val child = launch {
try{
println("The child is sleeping")
delay(Long.MAX_VALUE)
}finally{
println("The child is cancelled")
}
}
yield() // 使用yield来给我们的子作业一个机会来执行打印
println("Throwing an exception from the scope")
throw AssertionError()
}
}catch(e: AssertionError){
println("Caught an assertion error")
}
}
异常捕获的时机与位置:
使用CoroutineExceptionHandler对协程的异常进行捕获。
以下的条件被满足时,异常就会被捕获。
时机:异常是被自动抛出异常的协程所抛出的(使用launch,而不是async时)
位置:在CoroutineScope的CoroutineContext中或在一个根协程(CoroutineScope或者supervisorScope的直接子协程)中
fun `test coroutineExceptionHandler`() = runBlocking<Unit> {
val coroutineExceptionHandler = CoroutineExceptionHandler {_, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(coroutineExceptionHandler) {
throw AssertionError() //这个异常会被捕获到
}
val deferred = GlobalScope.async(coroutineExceptionHandler) {
throw ArithmeticException() //不会捕获到
}
job.join()
deferred.await()
}
异常捕获的常见错误:
fun `test coroutineExceptionHandler2`() = runBlocking<Unit> {
val coroutineExceptionHandler = CoroutineExceptionHandler {_, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(Job())
val job = scope.launch(coroutineExceptionHandler) {
launch{
throw IllegalArgumentException()
}
}
job.join()
}
fun `test coroutineExceptionHandler3`() = runBlocking<Unit> {
val coroutineExceptionHandler = CoroutineExceptionHandler {_, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(Job())
val job = scope.launch {
launch(coroutineExceptionHandler){
throw IllegalArgumentException()
}
}
job.join()
}
异常捕获阻止App闪退:
val coroutineExceptionHandler = CoroutineExceptionHandler {_, exception ->
println("Caught $exception")
}
findViewById<Button>(R.id.btn).also{
it.setOnClickListener{
GlobalScope.launch(handler){
Log.d("weip", "on Click")
"abc".substring(10)
}
}
}
Android中全局异常处理:
全局异常处理器可以获取到所有协程未处理的未捕获异常,不过它并不能对异常进行捕获,虽然不能阻止程序崩溃,全局异常处理器在程序调试和异常上报等场景中仍然有非常大的用处。
我们需要在classpath下面创建META-INF/services目录,并在其中创建一个名为kotlinx.coroutines.CoroutineExceptionHandler的文件,文件内容就是我们的全局异常处理器的全类名。
取消与异常:
取消与异常密切香瓜,协程内部使用CancellationException来进行取消,这个异常会被忽略。
当子协程被取消时,不会取消它的父协程。
如果一个协程遇到了CancellationException以外的异常,它将使用该异常取消它的父协程。当父协程的所有子协程都结束后,异常才会被父协程处理。
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()
}
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()
}
异常聚合:
当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。再第一个异常之后发生的所有其他异常,都将被绑定到第一个异常之上。
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)
println("Second child throws an exception")
throw IOException()
}
}
job.join()
}
文档来源: