前言
这篇博客不讲解协程原理,本着快速学习,快速理解,快速使用方式来讲解协程.
kotlin协程是什么? 它其实是类似android的Handler或者java的RxJava. 本质就是为了处理各个线程上的工作协调. 在实际的Android开发最经常的情况就是需要让子线程耗时处理的数据结果发布到主线程上的UI. 所以在需求的本质上就是一个线程+观察者模式的组合. 而观察者模式归根结底就是接口. 所以不管是Handler还是RxJava的实现都需要写大量的模版代码,而kotlin的协程更加简化.
参考 http://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html
依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
简单demo快速了解GlobalScope.launch
这里使用GlobalScope.launch一个新协程,GlobalScope是提供快速构建协程的方式. 协程处理用GlobalScope构建外还可以选择其他方式构建,下面会一一讲解到.
请注意,此代码是在Android平台下运行的,所以我不需要让主线程等待了.
fun demo() { GlobalScope.launch { delay(1000)//非堵塞线程的延迟一秒钟 Log.e(TAG, "当前线程id = " + Thread.currentThread().id) } Log.e(TAG, "主线程id = " + mainLooper.thread.id) }
结果
2021-10-15 15:23:38.687 21612-21612/com.example.myapplication E/ytzn: 主线程id = 2 2021-10-15 15:23:39.690 21612-21758/com.example.myapplication E/ytzn: 当前线程id = 3011
指定协程的线程类型
/** * 线程类型 */ fun threadType() { GlobalScope.launch(Dispatchers.Default) {//默认线程 Log.e(TAG, "Default当前线程id = " + Thread.currentThread().id) } GlobalScope.launch(Dispatchers.IO) {//IO线程 Log.e(TAG, "IO当前线程id = " + Thread.currentThread().id) } GlobalScope.launch(Dispatchers.Main) {//主线程 Log.e(TAG, "Main当前线程id = " + Thread.currentThread().id) } GlobalScope.launch(Dispatchers.Unconfined) {//不限于任何特定线程,就是创建的地方是什么线程,那么内部就是就是什么线程 Log.e(TAG, "Unconfined当前线程id = " + Thread.currentThread().id) } }
结果:
2021-10-23 17:59:51.986 29732-29768/com.example.myapplication E/ytzn: Default当前线程id = 951 2021-10-23 17:59:51.987 29732-29769/com.example.myapplication E/ytzn: IO当前线程id = 952 2021-10-23 17:59:51.992 29732-29732/com.example.myapplication E/ytzn: Unconfined当前线程id = 2 2021-10-23 17:59:51.997 29732-29732/com.example.myapplication E/ytzn: Main当前线程id = 2
协程的启动模式
一共有四种
CoroutineStart.DEFAULT //默认,会立即启动协程 CoroutineStart.LAZY //被动,需要调用start方法才能执行 CoroutineStart.ATOMIC //原子性,立即调度执行,并且开始执行前无法被取消,直到执行完毕或者遇到第一个挂起点suspend CoroutineStart.UNDISPATCHED //立即在当前线程执行协程体内容
DEFAULT
fun default() { GlobalScope.launch(start = CoroutineStart.DEFAULT) { Log.e("ytzn", "执行协程1") } GlobalScope.launch { Log.e("ytzn", "执行协程2") } //上面2个协程是一样的,在launch不传入启动模式时,默认DEFAULT模式 }
LAZY
fun lazy() { val job = GlobalScope.launch(start = CoroutineStart.LAZY) { Log.e("ytzn", "执行协程") } job.start() //需要调用start方法才能启动 job.cancel() //也可以取消 }
GlobalScope.async 带返回值协程
async方法其实与launch方法是一样的,唯一区别是async可以返回值.如果需要async返回值则必须在协程内部
代码:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) btn1.setOnClickListener { GlobalScope.launch { val random = async { return@async (1..10).random() }.await() //请注意别忘记了await方法,否则下面的log打印不会等待此返回值 Log.e("ytzn", "random = $random") } } }
结果:
2021-11-06 17:27:20.125 10056-10125/com.example.myapplication E/ytzn: random = 10
异常抛出
/** * 异常处理 */ suspend fun demo() { GlobalScope.launch(Dispatchers.Default) { throw RuntimeException() } val job = GlobalScope.async(Dispatchers.Default) { throw RuntimeException() } try { job.await() } catch (e: RuntimeException) { } }
协程的取消
在等待状态下的取消
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) btn1.setOnClickListener { GlobalScope.launch { val job = launch { Log.e("ytzn","A") delay(1000) Log.e("ytzn","B") delay(1000) Log.e("ytzn","C 因为被取消所以不会被打印") } delay(1500) Log.e("ytzn","取消协程") job.cancel() } } }
结果:
2021-11-09 21:21:56.459 8982-9270/com.example.myapplication E/ytzn: A 2021-11-09 21:21:57.469 8982-9269/com.example.myapplication E/ytzn: B 2021-11-09 21:21:57.968 8982-9269/com.example.myapplication E/ytzn: 取消协程
理解join()与cancelAndJoin()方法
join意思是阻塞等待协程结束。也就是说cancel执行后会马上返回,执行后续的代码,但是这个时候协程不一定结束。再调用join方法,这里表示阻塞等待协程结束。确保协程结束之后才执行后续的代码。我们也可以调用job.cancelAndJoin().
这里有两段代码可以验证join的功能
1.首先是不使用join方法的代码
btn1.setOnClickListener { GlobalScope.launch { val job = launch { Log.e("ytzn", "time 1 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 2 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 3 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 4 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 5 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 6 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 7 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 8 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 9 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 10 = ${System.currentTimeMillis()}") } Log.e("ytzn", "A") job.cancel() Log.e("ytzn", "B") Log.e("ytzn", "C") } }
在不调用join方法,可以看下面的结果,在打印日志 "C" 后依然执行了协程中的代码. 请注意,不调用join方法不是说必定会让 "C" 比 协程里的日志同步执行, 而是会有出现这种同步执行概率.
结果:
2021-11-24 17:34:56.862 30507-30557/com.example.myapplication E/ytzn: A 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 1 = 1637746496862 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 2 = 1637746496862 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 3 = 1637746496862 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 4 = 1637746496862 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 5 = 1637746496862 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 6 = 1637746496862 2021-11-24 17:34:56.862 30507-30557/com.example.myapplication E/ytzn: B 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 7 = 1637746496862 2021-11-24 17:34:56.862 30507-30557/com.example.myapplication E/ytzn: C 2021-11-24 17:34:56.862 30507-30558/com.example.myapplication E/ytzn: time 8 = 1637746496862 2021-11-24 17:34:56.863 30507-30558/com.example.myapplication E/ytzn: time 9 = 1637746496863 2021-11-24 17:34:56.863 30507-30558/com.example.myapplication E/ytzn: time 10 = 1637746496863
2.使用join方法的代码
btn1.setOnClickListener { GlobalScope.launch { val job = launch { Log.e("ytzn", "time 1 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 2 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 3 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 4 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 5 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 6 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 7 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 8 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 9 = ${System.currentTimeMillis()}") Log.e("ytzn", "time 10 = ${System.currentTimeMillis()}") } Log.e("ytzn", "A") job.cancel() Log.e("ytzn", "B") job.join() Log.e("ytzn", "C") } }
可以看到打印C的日志,一定是在最末尾的, 因为join的原因,需要让协程中的工作先完成在执行后续代码.
结果:
2021-11-24 17:44:41.510 32450-32497/com.example.myapplication E/ytzn: A 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 1 = 1637747081511 2021-11-24 17:44:41.511 32450-32497/com.example.myapplication E/ytzn: B 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 2 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 3 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 4 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 5 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 6 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 7 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 8 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 9 = 1637747081511 2021-11-24 17:44:41.511 32450-32496/com.example.myapplication E/ytzn: time 10 = 1637747081511 2021-11-24 17:44:41.514 32450-32496/com.example.myapplication E/ytzn: C
使用isActive让循环中的协程取消
btn1.setOnClickListener { GlobalScope.launch { val job = launch { var i = 0 while (isActive){ //如果这里的isActive 替代成 true 那么这段循环将不会被cancelAndJoin停止 i++ Log.e("ytzn","i = $i") } } job.cancelAndJoin() } }
使用yield方法让循环中的协程取消
GlobalScope.launch { val job = launch { var i = 0 while (true){ yield() i++ Log.e("ytzn","i = $i") } } job.cancelAndJoin() }
使用finally在取消协程的时候释放资源
GlobalScope.launch { val job = launch { try { var i = 0 while (isActive) { i++ Log.e("ytzn", "i = $i") } } finally { Log.e("ytzn", "释放资源") } } job.cancelAndJoin() }
结果:
2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: i = 1 2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: i = 2 2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: i = 3 2021-11-24 19:43:26.205 25980-26027/com.example.myapplication E/ytzn: 释放
suspend关键字
suspend关键字在协程开发中非常重要,在协程里是无法调用常规函数方法的会出现报错,所以,在协程里调用的函数方法需要增加suspend关键字.
suspend意思挂起,意思是在协程里调用此方法,将会优先执行此suspend函数方法的内部代码,将外部协程线程挂起.处理完成后在执行外部协程.下面的代码将验证这种说法:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) btn1.setOnClickListener { GlobalScope.launch { launch { delay(500) Log.e("ytzn","第二个执行"); } Log.e("ytzn","首先执行,验证不会被launch挂起") Log.e("ytzn","第三个执行 获取随机值= " + getRandomValue()) Log.e("ytzn","最后执行,验证会被suspend函数方法挂起") } } } suspend fun getRandomValue(): Int { delay(1000)//请注意这里是等待了1秒 return (1..10).random() }
请注意,在getRandomValue函数方法里是写了delay(1000) 等待一秒的,并且delay方法并不堵塞线程.但是外部的最后执行的log,依然是最后执行的.这就验证了suspend方法会挂起外部协程 .
结果:
2021-11-06 16:27:32.881 28729-28789/com.example.myapplication E/ytzn: 首先执行,验证不会被launch挂起 2021-11-06 16:27:33.386 28729-28807/com.example.myapplication E/ytzn: 第二个执行 2021-11-06 16:27:33.889 28729-28790/com.example.myapplication E/ytzn: 第三个执行 获取随机值= 2 2021-11-06 16:27:33.889 28729-28790/com.example.myapplication E/ytzn: 最后执行,验证会被suspend函数方法挂起
协程作用域coroutineScope
除了使用GlobalScope构建协程外,还可以使用 coroutineScope 构建器声明自己的作用域。而coroutineScope其实是GlobalScope的父类,GlobalScope只是封装了一些构建的快捷方便的函数
通过传入SupervisorJob上下文创建coroutineScope
用这种方法创建协程作用域的意义有2个:
1.可以创建一个目标线程的全局变量作用域,减少在创建目标线程的模版代码
2.方便取消全部作用域协程, 统一使用SupervisorJob调用释放协程,尽可能的减少因为大量使用协程却忘记释放而导致内存泄露
下面的代码里显示了这种用法,
class MainActivity3 : AppCompatActivity() { private val job = SupervisorJob() private val mMainScope = CoroutineScope(Dispatchers.Main + job) private val mIOScope = CoroutineScope(Dispatchers.IO + job) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main3) mMainScope.launch { while (isActive) { delay(100) Log.e("ytzn", "Main Time = ${System.currentTimeMillis()}") } } mIOScope.launch { while (isActive) { delay(100) Log.e("ytzn", "IO Time = ${System.currentTimeMillis()}") } } MainScope() button.setOnClickListener { job.cancel() //退出之前,取消job上下文控制的所有协程作用域 finish() } } }
如果你只需要一个主线程线程的协程作用域可以使用下面的代码快速创建
private val mMainScope = MainScope()
源码跟手动创建一样
@Suppress("FunctionName") public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
在协程内部构建coroutineScope的特性
fun demo1() { GlobalScope.launch { // 在后台启动一个新的协程并继续 Log.e(TAG,"首先执行") coroutineScope { // 创建一个协程作用域 launch { delay(500) Log.e(TAG,"第三个执行") } Log.e(TAG,"第二个执行") } Log.e(TAG,"第四个执行,需要等待coroutineScope协程作用域结束后执行") } }
结果:
2021-10-20 14:17:26.837 7519-7553/com.example.myapplication E/ytzn: 首先执行 2021-10-20 14:17:26.838 7519-7553/com.example.myapplication E/ytzn: 第二个执行 2021-10-20 14:17:27.340 7519-7554/com.example.myapplication E/ytzn: 第三个执行 2021-10-20 14:17:27.341 7519-7554/com.example.myapplication E/ytzn: 第四个执行,需要等待coroutineScope协程作用域结束后执行
堵塞线程式协程runBlocking
runBlocking在Android开发应该做到完全不使用, 但是在其他开发环境下可能会使用,它是堵塞主线程使其存活的重要方法。runBlocking是kotlin一个意义独特的函数, 官方并不推荐你使用它。看了它的代码注释你就能明白,注释如下:
/** * 运行一个新的协程,并可中断地阻塞当前线程,直到其完成。 * 此函数不应在协程中使用。 * 它旨在将常规的阻塞代码与以挂起风格编写的库连接起来,以便在主函数和测试中使用。 * Runs a new coroutine and blocks the current thread interruptibly until its completion. * This function should not be used from a coroutine. * It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests. */
调用了 runBlocking
的主线程会一直阻塞直到 runBlocking
内部的协程执行完毕。在下面的代码里是堵塞了Android的主线程,在实际项目里请勿操作
fun demo1() { GlobalScope.launch { // 在后台启动一个新的协程并继续 delay(1000L) Log.e(TAG, "Hello World!") } val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) Log.e(TAG, "Time1 = " + dateFormat.format(System.currentTimeMillis())) //主线程中的代码会立即执行 runBlocking { // 但是这个表达式阻塞了主线程 delay(2000L) // ……堵塞2秒 Log.e(TAG, "主线程id = " + mainLooper.thread.id) Log.e(TAG, "当前线程id = " + Thread.currentThread().id) Log.e(TAG, "Time2 = " + dateFormat.format(System.currentTimeMillis())) } Log.e(TAG, "Time3 = " + dateFormat.format(System.currentTimeMillis())) }
结果:
2021-10-18 21:03:37.265 27646-27646/com.example.myapplication E/ytzn: Time1 = 21:03:37 2021-10-18 21:03:38.265 27646-27715/com.example.myapplication E/ytzn: Hello World! 2021-10-18 21:03:39.271 27646-27646/com.example.myapplication E/ytzn: 主线程id = 2 2021-10-18 21:03:39.272 27646-27646/com.example.myapplication E/ytzn: 当前线程id = 2 2021-10-18 21:03:39.274 27646-27646/com.example.myapplication E/ytzn: Time2 = 21:03:39 2021-10-18 21:03:39.277 27646-27646/com.example.myapplication E/ytzn: Time3 = 21:03:39
runBlocking在Android开发不使用原因
runBlocking本质是堵塞主线程,设计用意也是让主线程等待数据结果。但是Android开发最重要是切记不可以堵塞主线程,哪怕是在主线程里处理耗时数据。runBlocking完全不契合Android开发。很多人会如下使用runBlocking:
fun runBlockingDemo() = runBlocking {
//模拟耗时处理
return@runBlocking "返回数据"
}
且不说上面的代码其实是有死锁的风险(在耗时处理里写了其他runBlocking并且频繁切换)。而且这完全是对kotlin协程理解不够,并且完全没阅读过它的注释。这行代码其实不写runBlocking也可以。因为完全等同于如下代码:
fun runBlockingDemo():String {
//模拟耗时处理
return "返回数据"
}
此外你还要可能在runBlocking使用的时候一不小心就将主线程堵塞,如下代码:
在这段代码里suspend方法里写了耗时处理或者延迟处理。 如果你不小心在runBlocking里调用了,就会让主线程堵塞。如果方法嵌套多一些,你自己都可能找不到写了延迟或者耗时处理的方法在哪里。
fun runBlockingDemo() = runBlocking {
val value = withContext(Dispatchers.Default){
return@withContext getDemoValue()
}
return@runBlocking value
}
suspend fun getDemoValue():String{
//模拟耗时
delay(2000)
return "模拟返回值"
}
withContext
kotlin 中 GlobalScope 类提供了几个创建协程的构造函数,在上面的讲解中已经提到了3个launch,async,runBlocking,下面会说下withContext与他们的区别
- launch: 创建一个新的协程
- async : 创建一个新的带返回值的协程,返回的是 Deferred 类
- withContext:不创建新的协程,指定协程上运行代码块,它只能在协程内部实现
- runBlocking:不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会
另外,witchContext可以让异步线程进行同步执行,比如下列代码中在主线程中等待子线程的结果返回,参考代码:
GlobalScope.launch(Dispatchers.Main) { Log.e("zh", "A") val awaitResult = withContext(Dispatchers.Default){ delay(1000) Log.e("zh", "B") return@withContext "awaitResult" } Log.e("zh", "C = ${awaitResult}") }
结果:
2022-12-09 13:56:18.529 19701-19701 zh A 2022-12-09 13:56:19.531 19701-19740 zh B 2022-12-09 13:56:19.532 19701-19701 zh C = awaitResult
协程中的同步锁
在协程中使用锁synchronized会出现如下错误提示
val mutex = Mutex() GlobalScope.launch { mutex.withLock { delay(1000) } }
在Activity中使用协程
导入 implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) test() } private fun test() { lifecycleScope.launch { val result = withContext(Dispatchers.IO) { delay(1000) "0" } Log.d("result", result) }
在ViewModel中使用协程
依赖implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
代码
class ReminderViewModel:ViewModel() { fun addRemindData(){ viewModelScope.launch(Dispatchers.IO) { } } }
End
本文来自博客园,作者:观心静 ,转载请注明原文链接:https://www.cnblogs.com/guanxinjing/p/15411291.html