Kotlin协程
Posted 一杯清泉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin协程相关的知识,希望对你有一定的参考价值。
要使用协程,需要额外引入指定的依赖,具体的版本可以查看google文档:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
参考:Android 上的 Kotlin 协程 | Android 开发者 | Android Developers
一、协程和线程的区别
协程是跑在线程上的,一个线程可以同时跑多个协程,每一个协程则代表一个耗时任务,我们手动控制多个协程之间的运行、切换,决定谁什么时候挂起,什么时候运行,什么时候唤,协程在线程中是顺序执行的。Thread中我们有阻塞、唤醒的概念,协程里同样也有,区别是Thread的阻塞是会阻塞线程的,而协程的挂起不会阻塞线程不影响后面的协程的执行。
二、协程的启动
1、协程的启动方式
(1)runBlocking:
启动一个新协程,该协程是阻塞的,直到其内部所有逻辑及子协程逻辑全部执行完成,才会执行后面的,使用在主线程会阻塞主线程,所以开发中通常不会使用。
(2)GlobalScope.launch:
启动了一个运行在子线程的顶层协程,协程的生命周期与应用程序一致。由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,使用不当会导致内存泄露。
(3)实现CoroutineScope + launch:
在应用范围内启动一个新协程,不会阻塞主线程,这是在应用中最推荐使用的协程使用方式,自己的组件实现CoroutieScope接口,在需要的地方使用launch方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。
2、子协程的启动方式
(1)aunch
异步启动一个子协程
(2)async
异步启动一个子协程,并返回Deffer对象,可通过调用Deffer.await()方法等待该子协程执行完成并获取结果,常用于并发执行-同步等待的情况。
class CoroutinesTest1
fun testRunBlocking()
runBlocking
Log.d(MainActivity.TAG, "方法1:runBlocking")
launch
Log.d(MainActivity.TAG, "启动了第1个子协程")
launch
Log.d(MainActivity.TAG, "启动了第2个子协程")
val async = async
Log.d(MainActivity.TAG, "启动了第3个子协程,有返回值")
"return--------我是第3个协程的返回值"
launch
Log.d(MainActivity.TAG, "启动了第4个子协程")
Log.d(MainActivity.TAG, async.await())
launch
Log.d(MainActivity.TAG, "启动了第5个子协程")
打印结果:
3、协程指定的线程
协程可以指定在某个线程中执行,可以在launch参数中指定,默认是子线程,不是主线程或者当前线程。
GlobalScope.launch(Dispatchers.Unconfined) ...
设置 CoroutineDispatcher 协程运行的线程调度器,有4种线程模式:
- Dispatchers.Default
- Dispatchers.IO -
- Dispatchers.Main - 主线程
- Dispatchers.Unconfined - 没指定,就是在当前线程
不写的话就是 Dispatchers.Default 模式的,或者我们可以自己创建协程上下文,也就是线程池,newSingleThreadContext 单线程,newFixedThreadPoolContext 线程池等等。
launch(newSingleThreadContext(""))
Log.d(MainActivity.TAG, "启动了第8个子协程---指定线程池newSingleThreadContext")
launch(Dispatchers.Main)
Log.d(MainActivity.TAG, "启动了第9个子协程---指定线程Main线程")
4、指定启动模式
不指定默认是立即启动,可以在launch中指定启动方式,提供了如下:
- DEAFAULT - 模式模式,不写就是默认
- ATOMIC -
- UNDISPATCHED
- LAZY - 懒加载模式,你需要它的时候,再调用启动,入下:
launch(Dispatchers.IO, start = CoroutineStart.LAZY)
Log.d(MainActivity.TAG, "启动了第9个子协程---指定线程IO线程")
.start()
三、协程的取消
launch返回Job,async返回Deffer,Job和Deffer都有以下方法:
- job.start() - 启动协程,除了 lazy 模式,协程都不需要手动启动
- job.join() - 等待协程执行完毕
- job.cancel() - 取消一个协程
- job.cancelAndJoin() - 等待协程执行完毕然后再取消
val launch6 = async
Log.d(MainActivity.TAG, "启动了第6个子协程")
delay(3000)
Log.d(MainActivity.TAG, "启动了第7个子协程,取消launch6")
launch6.cancel()
四、挂起函数suspend
协程里使用suspend关键字修饰方法,该方法可以被协程挂起,挂起函数挂起协程,并不会阻塞协程所在的线程,例如协程的delay()挂起函数会暂停协程一定时间,并不会阻塞协程所在线程,但是Thread.sleep()函数会阻塞线程。没用suspend修饰的方法不能参与协程任务,suspend修饰的方法只能在协程中只能与另一个suspend修饰的方法交流,需要注意的是suspend方法只能在协程里面调用, 不能在协程外面调用。
fun test()
Log.d(MainActivity.TAG, "方法3:实现CoroutineScope + launch")
launch
Log.d(MainActivity.TAG, "启动第1个协程")
launch
Log.d(MainActivity.TAG, "启动第2个协程")
Log.d(MainActivity.TAG, "suspend挂起函数")
val name = requestHost()
Log.d(MainActivity.TAG, "suspend挂起函数之后的数据:$name")
launch
Log.d(MainActivity.TAG, "启动第3个协程")
private suspend fun requestHost(): String
return async
"6666"
.await()
打印结果:
五、协程内部线程切换withContext
withContext这个函数主要可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。必须在协程或者suspend函数中调用,它必须显示指定代码块所运行的线程,它会阻塞当前上下文协程,会返回代码块的最后一行的值,如果最后一行没有返回值则返回Unit。
var withCxt = withContext(Dispatchers.IO)
delay(1000)
Log.d(MainActivity.TAG, "withContext---阻塞1秒")
"返回最后一一行"
Log.d(MainActivity.TAG, "withContext---阻塞1秒----之后执行$withCxt")
打印结果:
区别:
- launch、async:启动一个新协程
- withContext:不启动新协程,在原来的协程中切换线程,需要传入一个CoroutineContext对象
- withContext、async:都可以返回耗时任务的执行结果。 一般来说,多个withContext任务是串行的,且withContext可直接返回耗时任务的结果。 多个async任务是并行的,async返回的是一个Deferred<T>,需要调用其await()方法获取结果。
六、协程的内存泄露
协程使用不当也会引起内存泄露的问题,通过以下手段可以在使用中避免内存泄露的问题。
1、通过job.cancel()来取消
每当我们通过创建一个协程,就可以得到一个返回值job,然后我们在不需要协程的地方取消即可:job.cancel()
var job: Job = CoroutineScope(Dispatchers.Main).launch
......
fun onDestroy()
job.cancel()
2、通过MainScope()来处理
创建一个在主线程上面运行的、在主线程上面启动所有协程的CoroutineScope对象,然后在其子类里面使用。然后他同样可以在onDestroy()方法里面调用cancel()方法取消,避免协程泄露。
val baseScope = MainScope()
使用的时候
baseScope.launch
......
然后统一是在onDestroy()方法里面取消CoroutineScope对象
override fun onDestroy()
super.onDestroy()
baseScope.cancel()
因为baseScope是创建在主线程的,在主线程启动协程的对象,因此这里我们可以不用像GlobalScope.launch(Dispatchers.Main)和CoroutineScope(Dispatchers.Main).launch 那样指定线程,省略了Dispatchers.Main。
3、通过CoroutineScope接口实现
相比于上面的第二种方法,可以将协程提出到指定的类中,适合于更加解耦的情况,在需要启动的时候直接调用launch启动,也是在主线程。
class CoroutinesTest3() : CoroutineScope by MainScope(), LifecycleObserver
//手动销毁资源
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy()
cancel()
………………省略………………
4、通过viewModelScope对象
viewModelScope是依赖于ViewModel的,而且最新的AndroidSDK的ViewModel.kt已经帮我们创建好了,因此可以直接在ViewModel里面调用或者通过ViewModel对象来调用。
class HomeViewModel :ViewModel()
suspend fun getName(): List<String>
viewModelScope.launch
......
ViewModel的生命周期跟Activity/Fragment的生命周期是同步的,因此,viewModelScope对象创建的协程系统会自动帮我们管理好,不用我们去关心内存泄露问题。
5、通过lifecycleScope对象
lifecycleScope是Lifecycle的拓展函数,是Lifecycle对协程的支持,因此我们可以直接在Activity/Fragment调用lifecycleScope来创建、启动并管理协程,我们可以在任何生命周期中调用,优先推荐使用该方式。该功能需要额外引入ktx包:
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
参考:List of KTX extensions | Android Developers
除此之外,lifecycleScope还提供了更加精确的,带生命周期的创建函数、例如:
表示onCreated()方法被调用之后这里的协程才会被创建并启动的launchWhenCreated
lifecycleScope.launchWhenCreated
表示onStarted()方法被调用之后这里的协程才会被创建并启动的launchWhenStarted
lifecycleScope.launchWhenStarted :
表示onResumed()方法被调用之后这里的协程才会被创建并启动的launchWhenResumed
lifecycleScope.launchWhenResumed
lifecycleScope可以直接使用,也可以针对特定的生命周期控制,默认主线程,可以通过withContext来指定线程,通常有以下用法:
//在withContext中切换线程
lifecycleScope.launch
withContext(Dispatchers.IO)
//在launch时候指定线程
lifecycleScope.launch(Dispatchers.IO)
//不指定线程,默认主线程
lifecycleScope.launch
whenResumed
不指定线程,默认主线程
lifecycleScope.launchWhenResumed
whenResumed和launchWhenResumed执行时机一样,区别在于:
- whenResumed 可以有返回结果。
- launchWhenResumed 返回的是Job对象。
七、协程的异常处理
1、捕获的方式
如果协程内部没有通过try-catch处理异常,那么异常并不会被重新抛出或者被外部的try-catch捕获。异常将会在job层级结构中向上传递,将会被安装的CoroutineExceptionHandler处理,如果没有安装过,异常将会被线程的未捕获的异常处理器处理。
//通过这种方式是无法捕获协程的异常的
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch
try
throw RuntimeException("RuntimeException in coroutine")
catch (exception: Exception)
println("Handle $exception")
/**
* 协程的异常捕获
*/
fun testGlobalScope2()
//协程的异常处理
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch(coroutineExceptionHandler)
launch(coroutineExceptionHandler)
throw RuntimeException("协程发生异常")
private val coroutineExceptionHandler = CoroutineExceptionHandler() _, exception ->
Log.d(MainActivity.TAG, "Handle $exception in CoroutineExceptionHandler")
2、try-cach VS CoroutineExceptionHandler
如果你想要重试某些操作,或者在协程完成之前执行某些操作,那么可以考虑使用try-cach。需要注意的是,在协程内部使用了try-cach捕获该异常之后,那么这个异常将不会再向上传递,也不能使用利用结构性并发的取消函数。在i协程因异常结束需要打印异常信息的时候可以考虑使用CoroutineExceptionHandler。
3、coroutineScope的异常处理特性
coroutineScope会重新抛出失败子协程内的异常而不是将其继续向上传递,这样我就可以自己处理失败子协程的异常了。在try-cach内启动的协程内的异常不能被捕获,但如果在失败的协程外部套上coroutineScope函数,那就会不太一样了:
topLevelScope.launch
try
coroutineScope
launch
throw RuntimeException("RuntimeException in nested coroutine")
catch (exception: Exception)
println("Handle $exception in try/catch")
catch成功捕获了异常,这是因为coroutineScope将失败的子协程内部的异常抛出,而没有继续向上传递。
4、supervisorScope异常处理特性
作用域函数supervisorScope会在job层级中安装一个独立的新的子作用域,并使用SupervisorJob作为该作用域的job,这个新的作用域并不会将异常继续向上传递,异常将由它自己处理。在supervisorScope内直接启动的协程将作为顶级协程。顶级协程在由launch或者async启动的时候,它的表现和作为子协程时的表现将有所不同。
参考文章:
- https://blog.csdn.net/haoyuegongzi/article/details/108834032
- https://www.jianshu.com/p/d661a56031f4
- https://blog.csdn.net/zou8944/article/details/106447727
以上是关于Kotlin协程的主要内容,如果未能解决你的问题,请参考以下文章