Android Kotlin协程coroutine
Posted 孟芳芳
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Kotlin协程coroutine相关的知识,希望对你有一定的参考价值。
1.协程coroutine
协程是一种并发设计模式,在Kotlin中使用协程可以简化异步执行的代码,把异步回调代码同步化。
协程,其实就是相互协作的子程序,多个子程序之间通过一定的机制相互关联、协作地完成某项任务。比如一个协程在执行上可以被分为多个子程序,每个子程序执行完成后主动挂起,等待合适的时机再恢复;一个协程被挂起时,线程可以执行其它子程序,从而达到线程高利用率的多任务处理目的——协程在一个线程上执行多个任务,而传统线程只能执行一个任务,从多任务执行的角度,协程自然比线程轻量。
协程主要用来解决两个问题:
①处理耗时任务,这种任务常常会阻塞住主线程;
②保证主线程安全,即确保安全地从主线程调用任何suspend函数。
协程的优点:
①轻量。可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
②内存泄漏更少。协程使用结构化并发机制在一个作用域内执行多项操作。
③内置取消支持。取消操作会自动在运行中的整个协程层次结构内传播。
④Jetpack集成。许多Jetpack库都包含提供全面协程支持的扩展。ViewModel等还提供了协程作用域(比如viewModelScope),用于结构化并发。
看一个简单的协程:
class CoroutineTestActivity : Activity()
private val scope = MainScope()
private val textContent by lazy
findViewById<TextView>(R.id.tv_corout_name)
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_corout)
//通过MainScope,启动一个协程
scope.launch(Dispatchers.Main)
val data = obtainCacheData() //异步获取数据
textContent.text = data //更新UI
private suspend fun obtainCacheData(): String
//通过Dispatchers调度器,切换到子线程执行耗时操作
return withContext(Dispatchers.IO)
delay(8000)
"缓存数据" //返回的数据
该代码执行过程:
①执行onCreate函数,通过MainScope.launch在主线程上创建新协程,然后协程开始执行。
②在协程内,调用obtainCacheData()方法,这是一个suspend挂起方法,所以现在会挂起协程的进一步操作。直到obtainCacheData()里面的withContext块执行结束。
③withContext块执行结束后,onCreate中通过launch创建的协程恢复执行操作,返回数据。
可以看到,相对于一般的函数,协程多了2个状态:挂起(suspend)和恢复。挂起与阻塞不同,代码阻塞之后会一直停留在这里。而挂起是指到挂起点被挂起后,保存挂起点,主线程继续执行,当挂起函数执行完了,再恢复执行操作。
注:挂起函数只能在协程体内或者其它挂起函数内调用。
2.创建协程coroutine
首先在build.gradle中添加依赖 :
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
协程需要运行在协程上下文环境,在非协程环境中启动协程,有三种方式:
①runBlocking
启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "主线程id:$mainLooper.thread.id")
test()
Log.e(TAG, "协程执行结束")
private fun test() = runBlocking
repeat(8)
Log.e(TAG, "协程执行$it 线程id:$Thread.currentThread().id")
delay(1000)
runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。
②GlobalScope.launch
在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。不会阻塞当前线程。
由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁、创建销毁组件的场景。
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "主线程id:$mainLooper.thread.id")
val job = GlobalScope.launch
delay(6000)
Log.e(TAG, "协程执行结束 -- 线程id:$Thread.currentThread().id")
Log.e(TAG, "主线程执行结束")
运行结果:
主线程id:1
主线程执行结束
协程执行结束 -- 线程id:480
因此,从执行结果看出,launch不会阻断主线程。
③async/await:Deferred
async跟launch的用法基本一样,区别在于:async有返回值,返回值类型是是Deferred。async不会阻塞当前线程。async支持并发,并且一般跟await一起使用。
async和await是两个函数,这两个函数在使用过程中一般都是成对出现的。
async用于启动一个异步的协程任务,await用于得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的。
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch
val result = GlobalScope.async
delay(6000)
Log.e(TAG, "协程执行结束,线程id:$Thread.currentThread().id")
"hello world"
Log.e(TAG, "async结果result = " + result.await())
Log.e(TAG, "主线程执行结束")
运行结果:
主线程id:1
主线程执行结束
协程执行结束,协程id:48
async结果result=hello world
所以,async是不阻塞线程的,可以通过await()获取到async启动协程的结果。
多个async一般是可以同时进行的,即异步执行。
GlobalScope.launch
val asyncJob1 = async
Log.d(TAG, "任务1,线程id:$Thread.currentThread().id")
"第 1 个任务完成"
//asyncJob1.await()
val asyncJob2 = async
Log.d(TAG, "任务2,线程id:$Thread.currentThread().id")
"第 2 个任务完成"
没有中间的asyncJob1.await()时,运行结果可能任务2在前,也可能任务1在前,因为两个任务是在两个不同的线程里并行执行的。
加上中间的asyncJob1.await()后,就可以实现两个任务串行执行,永远先执行任务1,任务1完成后才会执行任务2。
有些时候可能需要调用多个接口,然后根据返回值再调用其他的接口。比如首先任务1和任务2并发执行,得到任务1和任务2的执行结果后,再去执行任务3,这时候也可以使用async+await实现。
GlobalScope.launch
//任务1和任务2并行执行
val asyncJob1 = async
delay(2000) //模拟任务1耗时
Log.d(TAG,"----第 1 个任务")
"第 1 个任务结果" //任务1返回基地国
val asyncJob2 = async
delay(2000)
Log.d(TAG,"----第 2 个任务")
"第 2 个任务结果"
//在这里使用await就是并行执行,不要在上面添加await(),否则就成了串行执行
val resultJob1 = asyncJob1.await()
val resultJob2 = asyncJob2.await()
//根据任务1和任务2的执行结果再去执行其他任务
withContext(Dispatchers.IO)
Log.d(TAG,"$resultJob1,$resultJob2,执行完成了----执行最后的任务")
运行结果:
----第 1 个任务
----第 2 个任务
第 1 个任务结果,第 2 个任务结果,执行完成了----执行最后的任务
大部分时候,执行耗时的串行操作的话,会使用WithContext,使异步代码同步化。并且它也不用开启一个新的协程:
return withContext(Dispatchers.IO)
delay(8000)
"缓存数据"
注意:await 的调用时机很重要,这影响到多个async任务是串行执行还是并发执行。下面通过测试耗时来验证一下:
第一种情况:
GlobalScope.launch
val time = measureTimeMillis
val asyncJob1 = async
delay(2000)
Log.d(TAG, "----第 1 个任务")
"第 1 个任务完成"
val asyncJob2 = async
delay(1000)
Log.d(TAG, "----第 2 个任务")
"第 2 个任务完成"
asyncJob1.await()
asyncJob2.await()
Log.d(TAG, "--measureTime耗时: $time")
第二种情况:
GlobalScope.launch
val time = measureTimeMillis
async
delay(2000)
Log.d(TAG, "----第 1 个任务")
"第 1 个任务完成"
.await()
async
delay(1000)
Log.d(TAG, "----第 2 个任务")
"第 2 个任务完成"
.await()
Log.d(TAG, "--measureTime2耗时: $time")
运行结果:
--measureTime耗时: 2180
--measureTime2耗时: 3178
通过上面结果可以看出,await()执行的时机很重要:measureTime()方法,就是并行执行的;measureTime2()方法,就是串行执行的。
3.launch
launch方法的定义:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
从方法定义中可以看出,launch() 是CoroutineScope的一个扩展函数,CoroutineScope就是协程的作用范围。
launch方法有三个参数:协程下上文;协程启动模式;协程体(block是一个带接收者的函数字面量,接收者是CoroutineScope)。
①协程下上文
上下文可以有很多作用,包括携带参数、拦截协程执行等。多数情况下不需要自己实现上下文,只需要使用现成的就好。
上下文有一个重要的作用就是线程切换,Kotlin协程使用调度器来确定哪些线程用于协程执行。调度器实现了CoroutineContext接口。
Kotlin提供的调度器:
1)Dispatchers.Main:在主线程上运行一个协程。可以用来更新UI 、调用挂起函数等。在UI线程中执行。
2)Dispatchers.IO:在主线程之外执行I/O操作(缓存、文件、数据库等数据)。在线程池中执行。
3)Dispatchers.Default:在主线程之外执行cpu密集型的工作(数据解析、数据计算等)。在线程池中执行。
4)Dispatchers.Unconfined:在调用的线程直接执行。
②启动模式
在Kotlin协程当中,启动模式定义在一个枚举类中。一共定义了4种启动模式:
public enum class CoroutineStart
DEFAULT, //默认的模式,立即执行协程体。
LAZY, //只有在需要的情况下运行
ATOMIC, //立即执行协程体,但在开始运行之前无法取消
UNDISPATCHED; //立即在当前线程执行协程体,直到第一个suspend调用
DEFAULT模式:创建协程后立即开始调度。在调度之前如果协程被取消,那么它就不会执行,而是以抛出异常来结束。
ATOMIC模式:创建协程后,根据协程的上下文,立即开始调度。协程在执行到第一个挂起点(挂起函数)之前,不能取消。
LAZY模式:只有协程被需要时,包括主动调用协程的start()/join()/await()等函数时,才开始。如果协程在被执行前取消,那么它就不会执行,而是以抛出异常来结束。
runBlocking
val job = launch(start = CoroutineStart.LAZY)
Log.d(TA, "LAZY start")
Log.d(TAG, "开始一些计算")
delay(3000)
Log.d(TAG, "耗时操作完成")
job.start()
运行结果:
开始一些计算
耗时操作完成
LAZY start
可以看到,只有调用了start()后才会打印。
UNDISPATCHED模式:协程创建后,立即在当前函数调用栈执行(在哪个线程创建,在哪个线程执行)。
这些模式特点:
1)DEFAULT、ATOMIC创建后,会立即调度(并不是立即执行);LAZY是只有触发了,才会执行;UNDISPATCHED会立即执行。
2)UNDISPATCHED执行的线程是创建它的函数所在线程,哪怕指定线程,也无效。
3)DEFAULT取消时,会被立即取消。
③协程体
协程体是一个用suspend关键字修饰的一个无参、无返回值的函数类型。被suspend修饰的函数称为挂起函数,与之对应的是关键字resume(恢复)。注意:挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。
suspend函数会将整个协程挂起,而不仅仅是这个suspend函数,也就是说一个协程中有多个挂起函数时,它们是顺序执行的。
举例:
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch
val token = getToken()
val userInfo = getUserInfo(token)
setUserInfo(userInfo)
repeat(3)
Log.e(TAG,"主线程执行$it")
private fun setUserInfo(userInfo: String)
Log.e(TAG, userInfo)
private suspend fun getToken(): String
delay(2000)
return "token"
private suspend fun getUserInfo(token: String): String
delay(2000)
return "$token - userInfo"
运行结果:
主线程执行0
主线程执行1
主线程执行2
token - userInfo
可见,getToken方法将协程挂起,协程中其后面的代码永远不会执行,只有等到getToken挂起结束恢复后才会执行。同时协程挂起后不会阻塞其他线程的执行。
Android Kotlin之Coroutine(协程)详解
协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。
在 Android 上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。
协程的优点:
- 轻量
您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。 - 内存泄漏更少
使用结构化并发机制在一个作用域内执行多项操作 - 内置取消支持
取消操作会自动在运行中的整个协程层次结构内传播。 - Jetpack 集成
许多 Jetpack 库都包含提供全面协程支持的扩展。ViewModel等还提供了协程作用域(比如,viewModelScope),用于结构化并发
协程主要解决的问题:
- 1,处理耗时任务。
- 2,保证主线程安全。
协程可以类似于RxJava那样,做到异步任务同步化,避免可能出现的回调地狱。同时,通过suspend关键字来标识函数,称为挂起函数,该函数,只能在协程或其他的suspend函数中执行,从而保证主线程的安全。
上面说了很多协程的好处。下面,我们先写个简单协程。
class CoroutineTestActivity : AppCompatActivity()
//也可以通过上面代理的方式实现(当然也可以直接用lifecycleScope)
private val scope = MainScope()
private val textContent by lazy
findViewById<TextView>(R.id.tv_corout_name)
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_corout_one)
//通过MainScope,启动一个协程
scope.launch(Dispatchers.Main)
//异步数据同步化,获取数据
val data = obtainCacheData()
//数据放到UI上
textContent.text = data
private suspend fun obtainCacheData(): String
//通过Dispatchers调度器,切换到子线程执行耗时操作
return withContext(Dispatchers.IO)
delay(8000)
"缓存数据"
该代码执行过程:
- 执行onCreate函数
- 通过MainScope.launch在主线程上创建新协程,然后协程开始执行。
- 在协程内,调用obtainCacheData(),现在会挂起协程的进一步操作。直到obtainCacheData()里面的withContext块,执行结束
- withContext块,执行结束后,onCreate中通过launch创建的协程恢复执行操作,返回数据。
这里可以看到相对于一般的函数,协程多了2个状态挂起(suspend)和恢复。
挂起跟阻塞的区别就是,代码阻塞之后,会一直停留在这里。挂起的话,就是到挂起点被挂起后,保存挂起点,主线程继续执行,当挂起函数执行完了,再回复执行操作。
挂起函数,obtainCacheData()被suspend修饰,也被称为挂起函数
挂起函数只能在协程体内或者其它挂起函数内调用
看下图
本文通过协程的作用域、启动,使用以及取消协程,最后,协程的异常处理顺序,进行梳理
一、结构化并发
协同程序遵循结构化并发原则。这意味着新的协程只能在协程作用域(CoroutineScope)里面启动,从而限定了协程的生命周期。从而避免了,因为某个协程的丢失造成的资源浪费(内存、CPU等)
优势:
- 取消指定的协程任务
- 处理异常信息
创建协程时,必须指定作用域,作用域可以取消,追踪所有有它创建的协程。
1.1,协程的作用域构建器
CoroutineScope跟runBlocking。
- runBlocking
常规函数,阻塞线程 - CoroutineScope
挂起函数,不阻塞线程。内部一个协程失败了,其它的协程也会被取消 - supervisorScope
挂起函数,不阻塞线程。内部一个协程失败了,不影响其它协程
他们都会等内部协程体及所有子协程执行完成。
runBlocking会阻塞线程
fun testRunBlocking()
runBlocking
launch
delay(2000)
Log.d("liu", "testRunBlocking 222222")
Log.d("liu", " testRunBlocking 111111")
//执行结果
testRunBlocking 222222
testRunBlocking 111111
CoroutineScope不阻塞线程
fun testCoroutineScope()
runBlocking
coroutineScope
launch
delay(2000)
Log.d("liu", "testCoroutineScope 222222")
Log.d("liu", "testCoroutineScope 111111")
...
//打印结果
testCoroutineScope 111111
testCoroutineScope 222222
CoroutineScope。如果内部一个协程失败了,其它的协程也会被取消
fun testCoroutineScope2()
runBlocking
coroutineScope
launch
delay(2000)
Log.d("liu", "testCoroutineScope2 222222")
launch
delay(1000)
Log.d("liu", "testCoroutineScope2 1111111")
val a = 2
val b = 0
val c = a / b
//执行结果
testCoroutineScope2 1111111
这里可以看出,第一个启动的协程job1没有打印
coroutineScope一个协程失败,其它协程也被取消
supervisorScope,内部一个协程失败了,不影响其它协程
fun testCoroutineScope3()
runBlocking
supervisorScope
launch
delay(2000)
Log.d("liu", "testCoroutineScope3 222222")
//这里换成了async。跟异常机制有关,方便测试
async
delay(1000)
Log.d("liu", "testCoroutineScope3 111111")
val a = 2
val b = 0
//这里会出现除数为0异常
val c = a / b
//执行结果
testCoroutineScope3 111111
testCoroutineScope3 222222
可以看到,async出异常后,第一个launch的协程继续执行了。
1.2,协程作用域
下面,看下常用的协程作用域(CoroutineScope)
- GlobalScope
- MainScope
- ViewModelScope
- LifecycleScope
1.2.1,GlobalScope
全局的作用域。Activity/Fragment销毁时,内部协程依然存在。
1.2.2,MainScope
在Activity中使用,可以再onDestroy中取消
代码示例
class CoroutineTestActivity : AppCompatActivity()
private val scope = MainScope()
private val textContent by lazy
findViewById<TextView>(R.id.tv_corout_name)
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_corout_one)
test1()
private fun test1()
scope.launch(Dispatchers.Main)
val data = obtainCacheData()
textContent.text = data
override fun onDestroy()
super.onDestroy()
scope.cancel()
1.2.3,ViewModelScope
在ViewModel中使用,绑定ViewModel的生命周期
应用中的每个 ViewModel 都定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel 处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作范围限定至 ViewModel,以便在 ViewModel 清除后,系统会自动取消工作以避免消耗资源。
您可以通过 ViewModel 的 viewModelScope 属性访问 ViewModel 的 CoroutineScope,如以下示例所示:
class MyViewModel: ViewModel()
init
viewModelScope.launch
// Coroutine that will be canceled when the ViewModel is cleared.
1.2.4,LifecycleScope
在Activty/Fragment中使用。绑定Activity/Fragment生命周期
每个 Lifecycle 对象都定义了 LifecycleScope。在此范围内启动的协程会在 Lifecycle 被销毁时取消。您可以通过 lifecycle.coroutineScope 或 lifecycleOwner.lifecycleScope 属性访问 Lifecycle 的 CoroutineScope。
以下示例演示了如何使用 lifecycleOwner.lifecycleScope 异步创建预计算文本:
class MyFragment: Fragment()
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default)
PrecomputedTextCompat.create(longTextContent, params)
TextViewCompat.setPrecomputedText(textView, precomputedText)
可重启生命周期感知型协程。当Lifecycle处于STARTED状态时,处理数据
class MyFragment : Fragment()
val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED)
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
viewModel.someDataFlow.collect
// Process item
详情点击,可重启生命周期感知型协程
二、协程的使用
最开始的例子,我们通过launch启动的协程,下面看看它的源码
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
这里可以看到,启动协程的时候,会有几个可选参数,我们不传,也会有默认值。
下面,我们就通过创建协程及它需要的几个参数(CoroutineContext,CoroutineStart等)来进行详细说明。
2.1,协程构建器
协程主要通过launch、async,这2种常用的构建方式。
2.1.1,通过launch启动协程
通过launch启动的协程,返回的是Job。并且没有返回结果
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
通过lauch启动的多个协程,可以使用join()函数,让协程按照顺序执行
看下面代码
private fun testJoin()
val job1 = launch
delay(1000)
Log.d("liu","----第 1 个任务")
val job2 = launch
delay(2000)
Log.d("liu","----第 2 个任务")
Log.d("liu","----第 3个任务")
//打印结果
//----第 3 个任务
//----第 1 个任务
//----第 2 个任务
如果,我们想要任务按照1、2、3的顺序执行的话,就需要使用join
代码如下
看下面代码
private fun testJoin2()
launch
val job1 =launch
delay(1000)
Log.d("liu", "----第 1 个任务")
job1.join()
val job2 = launch
delay(2000)
Log.d("liu", "----第 2 个任务")
job2.join()
Log.d("liu", "----第 3个任务")
//打印结果
//----第 1 个任务
//----第 2 个任务
//----第 3 个任务
最后,看下join()函数
public suspend fun join()
join()函数也是挂起函数,不会产生阻塞。
2.1.2,通过async启动协程
通过async启动的协程,返回的是Deffered,Job的子类。可以通过await()方法,获取最终的返回结果
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
...
public interface Deferred<out T> : Job
获取返回结果
private fun testAsync()
launch
val job = async
delay(2000)
"获取耗时数据"
Log.d("liu", "result: $job.await()")
//打印结果
result: 获取耗时数据
上面看到,通过await获取到了async启动协程的结果
async也想串行(按照顺序执行)的话,需要使用await()方法
private fun test4()
launch
val asyncJob1 = async
delay(1000)
Log.d("liu", "----先执行的任务")
"第 1 个任务完成"
asyncJob1.await()
val asyncJob2 = async
delay(2000)
Log.d("liu", "----后执行的任务")
"第 2 个任务完成"
...
//打印结果
----先执行的任务
----后执行的任务
很多时候,我们需要调用多个接口根据返回值,再调用其他的接口
使用async实现。
private fun test2()
launch
//开始并行执行
val asyncJob1 = async
delay(2000)
Log.d("liu","----第 1 个任务")
"第 1 个任务完成"
val asyncJob2 = async
delay(2000)
Log.d("liu","----第 2 个任务")
"第 2 个任务完成"
//这样就是并行执行,不要在上面,添加await(),否则,就成了串行执行
//async启动的协程,可以获取返回结果
val resultJob1 = asyncJob1.await()
val resultJob2 = asyncJob2.await()
withContext(Dispatchers.IO)
Log.d("liu","$resultJob1,$resultJob2,执行完成了----执行最后的任务")
...
//打印结果
----第 1 个任务
----第 2 个任务
第 1 个任务完成,第 2 个任务完成,执行完成了----执行最后的任务
这个函数,最后的打印结果就是*-第 1 个任务,第 2 个任务,执行完成了----执行最后的任务*
通过上面,我们可以看到通过async启动的协程,调用await()函数,可以拿到耗时数据的结果。
测试两种await()耗时情况
/**
* 测试耗时
*/
fun measureTime()
launch
val time = measureTimeMillis
val asyncJob1 = async
delay(2000)
Log.d("liu", "----第 1 个任务")
"第 1 个任务完成"
val asyncJob2 = async
delay(1000)
Log.d("liu", "----第 2 个任务")
"第 2 个任务完成"
asyncJob1.await()
asyncJob2.await()
Log.d("liu", "--measureTime耗时情况: $time")
fun measureTime2()
launch
val time = measureTimeMillis
async
delay(2000)
Log.d("liu", "----第 1 个任务")
"第 1 个任务完成"
.await()
async
delay(1000)
Log.d("liu", "----第 2 个任务")
"第 2 个任务完成"
.await()
Log.d("liu", "--measureTime2耗时情况: $time")
//结果
--measureTime耗时情况: 2180
--measureTime2耗时情况: 3178
通过上面结果,可以看出,await()执行的时机很重要
measureTime()方法,就是并行执行的
measureTime2()方法,就是串行执行的
最后看下await()函数
public suspend fun await(): T
await()函数也是一个挂起函数,不会产生阻塞
大部分时候,我们执行耗时的串行操作的话,会使用WithContext
return withContext(Dispatchers.IO)
delay(8000)
"缓存数据"
就想开始的代码一样,异步代码同步化。并且,它也不用开启一个新的协程
2.2,协程的调度器
调度器主要有3种(类似于RxJava的线程切换(Schedulers.newThread()等)?)
- Dispatchers.Default
- Dispatchers.Main
- Dispatchers.IO
Dispatchers.Default
非主线程,默认的调度器
主要是处理:CPU密集型操作(数据解析,数据计算等)
Dispatchers.Main
主线程
主要是处理:UI的更新操作,调用suspend挂起函数
Dispatchers.IO
非主线程
主要是处理:IO操作(缓存,文件,数据库等数据)及网络数据
Dispatchers.Unconfined,newSingleThreadContext(“MyOwnThread”)
详见:官方文档
2.3,协程的启动模式(CoroutineStart)
CoroutineStart定义在协程生成器的开始选项中。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
CoroutineStart的模式:
- CoroutineStart.DEFAULT
- CoroutineStart.ATOMIC
- CoroutineStart.LAZY
- CoroutineStart.UNDISPATCHED
开始介绍
CoroutineStart.DEFAULT
创建协程后,立即开始调度。
在调度之前如果协程被取消,那么它就不会执行,而是以抛出异常来结束。
看代码
fun coroutineStartOne()
runBlocking
//启动协程
val job = launch(start = CoroutineStart.DEFAULT)
Log.d("liu", "default start")
delay(3000)
Log.d("liu", "default end")
delay(1000)
//取消
job.cancel()
这里看到,协程被直接取消了。
CoroutineStart.ATOMIC
创建协程后,根据协程的上下文,立即开始调度。
协程在执行到第一个挂起点(挂起函数)之前,不能取消
fun coroutineStartATOMIC()
runBlocking
val job = launch(start = CoroutineStart.DEFAULT)
//TODO: 这里做一些耗时操作,完成之前,不会被取消
delay(3000)
Log.d("liu", "ATOMIC end")
job.cancel()
CoroutineStart.LAZY
只有协程被需要时,包括主动调用协程的start()/join()/await()等函数时,才开始
如果协程在被执行前取消,那么它就不会执行,而是以抛出异常来结束。
fun coroutineStartLAZY()
runBlocking
val job = launch(start = CoroutineStart.LAZY)
Log.d("liu", "LAZY start")
Log.d("liu", "开始一些计算")
delay(3000)
Log.d("liu", "耗时操作完成")
job.start()
//打印结果
开始一些计算
耗时操作完成
LAZY start
这里,可以看到,只有调用了start()后才会打印。
CoroutineStart.UNDISPATCHED
协程创建后,立即在当前函数调用栈执行(在哪个线程创建,在哪个线程执行)。在哪个函数创建,就在哪个线程执行,从名字可以看出,它不接受Dispatchers指定线程的调度
直到遇到第一个挂起点,与Dispatchers.Unconfined类似
与ATOMIC类似,即使被取消了,它也会执行到第一个挂起点?
fun coroutineStartUNDISPATCHED()
runBlocking
val job = async(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED)
Log.d("liu", "线程IO:$Thread.currentThread().name")
val job1 = async(context = Dispatchers.IO, start = CoroutineStart.DEFAULT)
Log.d("liu", "线程:$Thread.currentThread().name")
...
//打印结果
线程IO:main
线程:DefaultDispatcher-worker-1
这里可以看到。把启动模式换成DEFAULT后,线程也变成了IO线程。UNDISPATCHED只会在创建该协程指定的线程运行
这些模式特点:
- 1,DEFAULT,ATOMIC创建后,会立即调度(并不是立即执行);LAZY是只有触发了,才会执行;UNDISPATCHED会立即执行
- 2,UNDISPATCHED执行的线程是创建它的函数所在线程,哪怕指定线程,也无效
- 3,DEFAULT取消时,会被立即取消
2.4,协程的上下文(CoroutineContext)
CoroutineContext使用以下元素集定义协程的行为:
- Job:控制协程的生命周期。
- CoroutineDispatcher:将工作分派到适当的线程。
- CoroutineName:协程的名称,可用于调试。
- CoroutineExceptionHandler:处理未捕获的异常。
对于在作用域内创建的新协程,系统会为新协程分配一个新的 Job 实例(不会继承),其他 CoroutineContext 元素(会被继承)。可以通过向 launch 或 async 函数传递新的 CoroutineContext 替换继承的元素。请注意,将 Job 传递给 launch 或 async 不会产生任何效果,因为系统始终会向新协程分配 Job 的新实例。
- 下面代码,展示组合的使用方式
/**
* 协程上下文的组合使用
*/
private suspend fun testCoroutineContext()
launch(Job() + Dispatchers.Default + CoroutineName("coroutine new Name"))
println(" Thread info: $Thread.currentThread().name")
这里我们看到上下文,都是通过“+”号连接的,对Kotlin不陌生的话,我们知道它应该是重写了"+"操作符,看下吧
public interface CoroutineContext
...
public operator fun plus(context: CoroutineContext): CoroutineContext =
- 展示协程上下文的继承关系
private suspend fun testCoroutineContext2()
val exceptionHandler = CoroutineExceptionHandler _, exception ->
Log.d("liu", "exception: $exception")
Log.d("liu", "Top Job exceptionHandler: $exceptionHandler")
//创建Job
val topJob = Job()
//创建一个新的协程作用域
val scope = CoroutineScope(topJob + Dispatchers.Default + CoroutineName("coroutine new Name") + exceptionHandler)
//打印顶层Job
println("Top Job Info: $topJob")
val job = scope.launch()
//打印协程相关信息
Log.d("liu", "Job Info: $coroutineContext[Job] $coroutineContext[CoroutineName] $coroutineContext[CoroutineExceptionHandler] , Thread info: $Thread.currentThread().name")
val job2 = async
Log.d("liu", "Job Info: $coroutineContext[Job] $coroutineContext[CoroutineName] $coroutineContext[CoroutineExceptionHandler] , Thread info: $Thread.currentThread().name")
job2.await()
job.join()
//打印结果
//异常Handler
Top Job exceptionHandler: cn.edu.coroutine.CoroutineTestActivity$testCoroutineContext2$$inlined$CoroutineExceptionHandler$1@6c29d69
//Job名字
Top Job Info: JobImplActive@50933ee
//launch打印 job 协程名字 exceptionHandler 线程信息
Job Info: StandaloneCoroutineActive@ad1d08f CoroutineName(coroutine new Name) cn.edu.coroutine.CoroutineTestActivity$testCoroutineContext2$$inlined$CoroutineExceptionHandler$1@6c29d69 , Thread info: DefaultDispatcher-worker-2
//launch打印 job 协程名字 exceptionHandler 线程信息
Job Info: DeferredCoroutineActive@67b8f1c CoroutineName(coroutine new Name) cn.edu.coroutine.CoroutineTestActivity$testCoroutineContext2$$inlined$CoroutineExceptionHandler$1@6c29d69 , Thread info: DefaultDispatcher-worker-1
这里,可以明显的看到exceptionHandler 是同一个(ExceptionHandler$1@6c29d69),Job都是不同的,Name是一样的(coroutine new Name),线程信息都是IO的。
通过这里可以看到 通过launch 或 async启动的协程,系统始终会向新协程分配 Job 的新实例。所以,Job是不会传递到子协程的,但是,其它的属性,都可以被继承下来
传递关系: CoroutineScope—launch—async
协程上下文的继承关系: 1,首先,使用本身传入的参数;2,使用继承过来的参数;3,使用默认参数
2.5,Job的生命周期
Job的状态有:
- 创建(New)
- 活跃(Active)
- 完成中/进行中(Completing)
- 取消中(Cancelling)
- 已取消(Cancelled)
- 已完成(Compeleted)
官方地址:点击这里
Job的状态上面已经介绍了,虽然无法直接判断这些状态。不过,我们可以根据它的属性isActive、isCompleted、isCancelled,来判断协程的状态。
下面是Job状态跟属性(isActive、isCompleted、isCancelled)的关系图
State | 【 isActive 】 | 【 isCompleted 】 | 【 isCancelled 】 |
---|---|---|---|
创建(New) | false | false | false |
活跃(Active) | true | false | false |
完成中(Completing) | true | false | false |
取消中(Cancelling | false | false | true |
已取消(Cancelled) | false | true | true |
已完成(Compeleted) | false | true | false |
比如,协程正常完成的话,isCompleted=true & isCancelled =false;
通常,Job创建的时候处于活跃状态(创建并启动)。但是,当协程构建的启动模式为CoroutineStart.LAZY时,协程的状态就处于创建状态(New),通过调用join或者start让Job处于活跃(Active)状态
官方配图
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
2.6,取消协程
取消协程的主要内容有:
- 取消协程的作用域
- 取消协程
单独的协程取消,不会影响其余的兄弟协程- CPU密集型任务的取消
- isActive
- ensureAction
- yield
- CPU密集型任务的取消
- 取消协程抛出的异常
2.6.1,取消协程的作用域
取消协程的作用域,会取消它的子协程
fun coroutineScopeCancel()
//等待子协程执行完
runBlocking<Unit>
//CoroutineScope不会继承runBlocking的属性。需要delay或者join
val scope = CoroutineScope(Dispatchers.Default)
scope.launch
delay(1000)
Log.d("liu","启动 job 1")
scope.launch
delay(1000)
Log.d("liu","启动 job 2")
//需要挂起,等待scope执行完
delay(2000)
不加delay的话,scope不会执行
fun coroutineScopeCancel()
//等待子协程执行完
runBlocking<Unit>
//CoroutineScope不会继承runBlocking的属性。需要delay或者join
val scope = CoroutineScope(Dispatchers.Default)
scope.launch
delay(1000)
Log.d("liu","启动 job 1")
scope.launch
delay(1000)
Log.d("liu","启动 job 2")
//需要挂起,等待scope执行完
delay(200)
scope.cancel()
delay(2000)
这样什么也不会打印,可以看出通过scope已经取消了其所有子协程
2.6.2,取消协程
协程的任务被取消后,不会影响兄弟协程的任务
普通协程的取消
fun coroutineCancel1()
runBlocking
val job1 = launch
delay(1000)
Log.d("liu","启动 job 1")
val job2 = launch
delay(1000)
Log.d("liu","启动 job 2")
job1.cancel()
...
//打印结果
启动 job 2
这里可以看出。job1被取消后,job2还是会执行
CPU密集型(计算)任务的取消
有2种方法可以取消计算代码的任务。
第一种方法是周期性地调用检查取消的挂起函数(借助Job生命周期,通过ensureActive()函数)。
另外一种就是通过yield()函数,明确检查Job是否处于取消状态.
下面,我们分别来看下
- 借助Job生命周期处理
CPU密集型任务(计算量大),通过cancel是无法及时取消的。这个时候,我们可以通过Job的生命周期的辅助来帮助我们取消计算
案例
我们每个一秒,打印一次Log日志。然后,中途取消
看下代码
fun coroutineCancelCPUIntensiveTask1()
runBlocking
val startTime = System.currentTimeMillis()
val job = launch
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5)
if (System.currentTimeMillis() >= nextTime)
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
通过执行结果,可以看到,执行取消后,计算仍然是完成了之后,才会取消
这个时候,我们通过Job的生命周期知道,在执行cancel后,Job的状态State就变成了Cancelling。
而Cancelling对应的我们可访问的Job属性isActive就变成了false。
这样,我们在while循环中,通过添加isActive是正在计算(是否为true),来辅助我们进行计算,以便在取消(Cancel)后,可以及时退出。
代码示例
fun coroutineCancelCPUIntensiveTask2()
runBlocking
val startTime = System.currentTimeMillis()
val job = launch
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5 && isActive)
if (System.currentTimeMillis() >= nextTime)
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
- 通过ensureActive()来处理
在协程执行过程中,我们也可以通过ensureActive()函数,来确认协程是否是活跃状态。如果是非活跃状态的话,它会抛出协程特有的异常来取消协程。
示例代码
fun coroutineCancelCPUIntensiveTask3()
runBlocking
val startTime = System.currentTimeMillis()
val job = launch
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5 )
ensureActive()
if (System.currentTimeMillis() >= nextTime)
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
这段代码也是可以取消的。跟上面代码类似。取消isActive判断,添加ensureActive()判断。
其实ensureActive()函数,内部也是通过抛出异常来处理的,只不过是被静默处理了。后面会详细说明协程的异常处理。
看下ensureActive()函数的代码
public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive()
调用协程上下文的ensureActive()函数
public fun CoroutineContext.ensureActive()
get(Job)?.ensureActive()
调用了Job的ensureActive()函数
public fun Job.ensureActive(): Unit
if (!isActive) throw getCancellationException()
getCancellationException()。估计就是获取,我们Cancel()时的可选异常
public fun getCancellationException(): CancellationException
这里可以看到,如果不是isActive活跃状态的话,就抛出了异常CancellationException。
CancellationException这个异常在我们执行Cancel()的时候,是可选参数。
到这里,我们就知道了,当我们想取消而无法取消协程时,我们也是可以通过,主动抛出这个异常来取消的,因为这个异常,协程会静默处理掉(上面普通协程取消时候,已经分析过)。
- 通过yield()函数来处理
官方地址:戳一下
yield()函数会检查协程状态(也是通过判断job是否是活跃状态),如果是非活跃状态,也会抛出异常,取消协程。
此外,它还会尝试出让线程的执行权,给其它协程提供执行机会。
首先,我们先看下yield()函数
public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ uCont ->
val context = uCont.context
context.checkCompletion()
....
接着看
internal fun CoroutineContext.checkCompletion()
val job = get(Job)
if (job != null && !job.isActive) throw job.getCancellationException()
这里,看到如果job不存在或者处于非活跃状态(!isActive)的话,就抛出了job.getCancellationException()异常。这个函数,我们在看ensureActive()时,刚看了,就是CancellationException异常。
所以,yield()函数也会检查协程是否处于活跃状态,不是的话,直接抛异常取消。
代码示例
fun coroutineCancelCPUIntensiveTaskYield()
runBlocking
val startTime = System.currentTimeMillis()
val job = launch
var nextTime = startTime
var totalPrintCount = 0
while (totalPrintCount < 5 )
yield()
if (System.currentTimeMillis() >= nextTime)
nextTime = 1000
totalPrintCount++
Log.d("liu", "+++日志打印: $totalPrintCount")
delay(1000)
Log.d("liu", "开始执行取消")
job.cancel()
Log.d("liu", "执行取消完成")
在计算量特别大的时候,尽量使用yield()函数
取消协程抛出的异常
取消协程是通过抛出异常(CancellationException)来取消的。
在cancel的时候,是可以传入我们定义的异常的。但是,没有传入的话,为什么也没有发现异常?
这个是Kotlin内部已经自己处理了。
我们看下Job的cancel()方法
public interface Job : CoroutineContext.Element
...
public fun cancel(cause: CancellationException? = null)
...
可以看出,当我们cancel的时候,是一个可选型的函数的
我们通过try捕获一下异常
fun coroutineCancelException()
runBlocking
runBlocking
val job1 = launch
try
delay(1000)
Log.d("liu", "启动 job 1")
catch (e: CancellationException)
e.printStackTrace()
val job2 = launch
delay(1000)
Log.d("liu", "启动 job 2")
job1.cancel()
我们再自定义一个异常看看
fun coroutineCancelException()
runBlocking
runBlocking
val job1 = launch
delay(1000)
Log.d("liu","启动 job 1")
val job2 = launch
delay(1000)
Log.d("liu","启动 job 2")
job1.cancel(CancellationException("主动抛出异常"))
打印结果
主动抛出异常
我们的异常信息主动抛出异常,被打印出来了
2.7,取消协程后,资源释放
2.7.1,捕获异常释放资源
上面取消协程时,我们讲了,取消协程时通过抛出异常来实现的。
我们可以使用try…catch来捕获这个异常。那么,如果,我们有需要释放的资源,也可以通过try…catch…finally,在finally中来释放我们的资源.
fun coroutineCancelResourceRelease()
runBlocking
runBlocking
val job1 = launch
try
delay(1000)
Log.d("liu", "启动 job 1")
finally
Log.d("liu", "finally job 1")
val job2 = launch
delay(1000)
Log.d("liu", "启动 job 2")
job1.cancel()
这里,可以看到finally中的代码执行了。如果,我们想释放资源的话,我们可以try挂起点,在finally中释放资源.
2.7.2,通过use()函数,释放资源
该函数只能被实现了Closeable的对象使用,程序结束的时候会自动调用close()方法,适合文件对象
不是use()函数
fun coroutineCancelResourceReleaseByUse()
runBlocking
val buffer = BufferedReader(FileReader("xxx"))
with(buffer)
var line: String?
try
while (true)
line = readLine() ?: break;
Log.d("liu", line)
finally
close()
需要自己在finally中关闭资源
使用use()函数
fun coroutineCancelResourceReleaseByUse()
runBlocking
BufferedReader(FileReader("xxx")).use
while (true)
val line = readLine() ?: break;
Log.d("liu", line)
不需要我们自己关闭。看下use函数
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R
var exception: Throwable? = null
try
return block(this)
...
finally
...
try
close()
catch (closeException: Throwable)
// cause.addSuppressed(closeException) // ignored here
....
该函数已经在finally中实现了,该功能
2.7.3,NonCancellable-取消中的挂起函数
一个NonCancellable的Job总是处于活跃状态。它是为withContext()函数设计的。以防止取消时,需要在不取消的情况下执行的代码块。
比如,协程任务执行的失败。调用接口通知后台等情况。
NonCancellable该对象不适用于launch、async和其它的协程构建者。如果,你再launch中使用的话,那么当父协程取消的时候,不仅新启动的子协程不会被取消,父级和子级的关系也会被切断。父级不会等着子级执行完成,也不会在子级异常后取消。
官方文档:请戳这里
withContext(NonCancellable)
// 这里执行的代码块,不会被取消
一般情况下,在取消协程时,如果,我们通过try…finally捕获后,在finally中释放资源。
如果,finally中有挂起函数的话,那么该函数是不能执行的(协程处于非活跃状态)
处于取消中状态的协程不能使用挂起函数,当协程被取消后需要调用挂起函数的话,就需要通过NonCancellable来让挂起函数处于活跃状态,这样会挂起运行中的代码。
示例示例代码
fun coroutineUnCancelException()
runBlocking
runBlocking
val job1 = launch
try
repeat(1000) i ->
delay(1000)
Log.d("liu", "启动 job 1,index: $i")
catch (e: CancellationException)
e.printStackTrace()
finally
delay(2000)
Log.d("liu", "finally job 1")
job1.cancel()
在取消时,finally中也有挂起函数(就想任务成功/失败,都通知后台一样)
这个时候,finally里面的日志是打印不出来的
我们需要用到NonCancellable
示例
fun coroutineUnCancelException2()
runBlocking
runBlocking
val job1 = launch
try
repeat(1000) i ->
delay(1000)
Log.d("liu", "启动 job 1,index: $i")
finally
//在cancel里,有挂起函数后,需要用到NonCancellable
withContext(NonCancellable)
delay(2000)
Log.d("liu", "finally job 1")
job1.cancel()
这样的话,finally中的日志就能成功打印了。如果,我们需要在出现异常时候,调用网络请求等挂起函数的话,可以通过这种方式来完成。
2.8,超时任务的取消(withTimeout)
取消协程执行的最实际的原因就是它执行时间超时,比如,网络请求等。
虽然,我们可以有其它的方式追踪取消这样的任务,但是,我们可以直接使用withTimeout这个函数,达到同样的效果
官方地址:请戳这里
示例如下:
withTimeout(1300L)
repeat(1000) i ->
println("I'm sleeping $i ...")
delay(500L)
打印结果
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
之前,我们取消协程也会抛出异常,都会是被静默处理的。
这里,我们可以看到,超时任务会抛出TimeoutCancellationException异常,是没有被静默处理的。
但是,有时候,我们不希望抛出异常,我们希望返回Null或者一个默认值。那么,我们就需要用到withTimeoutOrNull。
示例代码:
fun coroutineTimeOutOrNull()
runBlocking
launch
val result = withTimeoutOrNull(1300L)
repeat(1000) i ->
Log.d("liu", "I'm sleeping $i ...")
delay(500L)
"执行完成"
?: "执行未完成,默认值"
Log.d("liu","Result is $result")
这里,我们看到,返回的就是一个默认值。
三、协程异常处理
我们知道,取消异常实在挂起点抛出CancellationException异常,协程的机制会忽略/静默处理它。
这里,我们就看下如果协程在取消过程中出现异常或者同一个协程的多个子协程发生异常的处理机制。
主要内容包括:
- 协程异常的传播
- 自动传播异常与用户暴露异常
- 非根协程的异常传播
- supervisorJob
- 协程的异常处理
3.1,协程异常的传播
根据协程构建器的方式,异常有2种方式:自动异常传播(launch/actor)和向用户公开异常(async/produce)。
当这些构建器用于创建根协程时(不是另一个协程的子协程),前者这些构造器(launch/actor)会将异常视为未捕获异常,类似Java的Thread.uncaughtExceptionHandler;而后者(async/produce)则依赖用户来最终处理这些异常。
官方地址:戳一戳
3.1.1,根协程的异常传播(自动传播异常跟用户公开异常)
上面介绍的都是跟协程的异常传播方式,下面来看下
/**
* 根协程异常的传播
*/
private fun handleException()
runBlocking
val job = GlobalScope.launch
try
Log.d("liu", "launch 抛出未捕获异常")
throw IndexOutOfBoundsException()
catch (e: java.lang.IndexOutOfBoundsException)
Log.d("liu", "launch 捕获 IndexOutOfBoundsException")
job.join()
val deferred = GlobalScope.async
Log.d("liu", "async 抛出向用户公开的异常")
throw ArithmeticException()
try
deferred.await()
catch (e: ArithmeticException)
Log.d("liu", "async 捕获 ArithmeticException")
打印结果
launch 抛出未捕获异常
launch 捕获 IndexOutOfBoundsException
async 抛出向用户公开的异常
async 捕获 ArithmeticException
从上面的代码及打印结果,可以看出
launch启动的协程,我们是在异常点捕获的异常(类似java的未捕获异常)
async启动的协程,我们是在await()这里捕获的异常,它是想用户公开的异常。如果,不调用await()函数的话,这个异常是不会抛出来的。
3.1.2,非根协程的异常传播
非根协程中的异常,总是会传播的
示例代码
fun handleExceptionByNoteRootCoroutine()
runBlocking
launch
async
Log.d("liu", "async 抛出异常")
throw ArithmeticException()
这里执行后,会直接抛出异常。通过这里可以看到,async通过根协程启动的话,不调用await(),是不会抛出异常的;通过非根协程启动的话,不管调不调await()函数,都会抛出异常。
这里,async抛出的异常会直接跑到launch里,抛出未捕获的异常。
3.1.3,异常的传播特性
当一个协程由于一个异常而执行失败时,它会把异常传递到它的父级。父级会做一下操作:
- 取消其的子级协程
- 取消它自己
- 将异常传播给其父级
如果是这样的话,一个子协程失败,跟它有关联的协程都会失败,这样并不符合我们的预期。
如果想要打破这种传播特性的话,我们可以使用supervisorJob
3.1.4,supervisorJob,supervisorScope
在supervisorScope内运行的子例程不会将异常传播到其父级。所以,如果我们不想让异常不断的向父级传播的话,可以使用supervisorScope或supervisorJob.
官方地址:戳一下
相对于coroutineScope,我们可以使用supervisorScope用于并发。它仅在一个方向上传播取消,并仅在自身失败时取消所有子项(向下传递)。它也会像coroutineScope一样等待所有孩子完成。
Supervision job
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor))
// launch the first child -- its exception is ignored for this example (don't do this in practice!)
val firstChild = launch(CoroutineExceptionHandler _, _ -> )
println("The first child is failing")
throw AssertionError("The first child is cancelled")
// launch the second child
val secondChild = launch
firstChild.join()
// Cancellation of the first child is not propagated to the second child
println("The first child is cancelled: $firstChild.isCancelled, but the second one is still active")
try
delay(Long.MAX_VALUE)
finally
// But cancellation of the supervisor is propagated
println("The second child is cancelled because the supervisor was cancelled")
// wait until the first child fails & completes
firstChild.join()
println("Cancelling the supervisor")
supervisor.cancel()
secondChild.join()
打印结果
The first child is failing
The first child is cancelled: true, but the second one is still active
Cancelling the supervisor
The second child is cancelled because the supervisor was cancelled
Supervision scope
try
supervisorScope
val child = launch
try
println("The child is sleeping")
delay(Long.MAX_VALUE)
finally
println("The child is cancelled")
// Give our child a chance to execute and print using yield
yield()
println("Throwing an exception from the scope")
throw AssertionError()
catch(e: AssertionError)
println("Caught an assertion error")
打印结果
The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error
普通的Job跟supervisor job的区别就是异常处理。每个孩子都应该通过异常处理机制自行处理其异常,不同之处在于:孩子的失败不会传播到父级(向下传递)。这意味着直接在supervisorScope内启动的协程,在其作用域内使用CoroutineExceptionHandler跟使用根协程一样。
val handler = CoroutineExceptionHandler _, exception ->
println("CoroutineExceptionHandler got $exception")
supervisorScope
val child = launch(handler)
println("The child throws an exception")
throw AssertionError()
println("The scope is completing")
println("The scope is completed")
打印结果
The scope is completing
The child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
The scope is completed
防止APP闪退,协程的异常处理CoroutineExceptionHandler
在协程上下文中的可选元素,用于异常捕获。协程的异常捕获使用的是CoroutineExceptionHandler来进行异常捕获。
CoroutineExceptionHandler仅在未捕获的异常(未以任何其他方式处理的异常)上调用。特别是,所有子协程(在另一个Job的上下文中创建的协程)都将其异常的处理委托给其父协程,父协程也委托给父协程,依此类推,直到根,因此永远不会使用安装在子协程上下文中的CoroutineExceptionHandler。除此之外,通过asynce构建的协程总是捕获所有异常,并在生成的Deferred对象中表示它们,因此它使用CoroutineExceptionHandler不起作用。
我们知道通过CoroutineExceptionHandler可以捕获异常。但是,不是所有的异常都可以被捕获,需要满足一下条件:
- 异常是被自动抛出异常的协程抛出(launch启动,而不是async)
- 在CoroutineScope的CoroutineContext中或根协程(CoroutineScope或supervisorScope)的直接子协程
代码示例
fun test()
val handler = CoroutineExceptionHandler _, exception ->
Log.d("liu", "CoroutineExceptionHandler got $exception")
runBlocking
val jo以上是关于Android Kotlin协程coroutine的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin Coroutine,Android Async Task 和 Async await 的区别
Kotlin Coroutine 源码解析 —— 协程是如何运行的