记录一下Kotlin中协程的定义和协成的使用,主要是面试准备,简单记录于此,方便自己查阅和学习。
正文
协程
协程是Kotlin提供的轻量级并发方案,通过suspend挂起函数实现非阻塞异步编程,配合CoroutineScope和Dispatcher,在Android中用于安全地执行网络、数据库等异步任务。
协程主要还是运行在线程中的,因此协程不能代替线程
协程挂起
delay()函数类似于Thread.sleep(),都表示使程序延迟操作,但是delay()函数不会阻塞线程,只是挂起协程本身。
当协程在等待时,线程将返回到线程池中,当协程等待完成时,将使用在线程池中的空闲线程来恢复程序继续执行。
Log.d(TAG, "1 打印前的线程${Thread.currentThread()}") CoroutineScope(Dispatchers.IO).launch { Log.d(TAG, "2 打印前的线程${Thread.currentThread()}") delay(1000L) Log.d(TAG, "3 打印前的线程${Thread.currentThread()}") } // 启动另外一个协程 CoroutineScope(Dispatchers.IO).launch { delay(2000L) Log.d(TAG, "4 打印前的线程${Thread.currentThread()}") } Log.d(TAG, "5 打印前的线程${Thread.currentThread()}") Thread.sleep(2000L)
打印日志
1 打印前的线程Thread[main,5,main] 5 打印前的线程Thread[main,5,main] 2 打印前的线程Thread[DefaultDispatcher-worker-1,5,main] 3 打印前的线程Thread[DefaultDispatcher-worker-1,5,main] 4 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
协程运行在线程中
所以协程替代不了线程
协程延迟后,会从线程池中找一个空闲的线程恢复执行
上面2个协程可能在同一个线程中执行
如果第二个协程delay时间改为delay(1000L)后,下面打印
//第一次 1 打印前的线程Thread[main,5,main] 5 打印前的线程Thread[main,5,main] 2 打印前的线程Thread[DefaultDispatcher-worker-1,5,main] 4 打印前的线程Thread[DefaultDispatcher-worker-1,5,main] 3 打印前的线程Thread[DefaultDispatcher-worker-2,5,main] //第二次 1 打印前的线程Thread[main,5,main] 5 打印前的线程Thread[main,5,main] 2 打印前的线程Thread[DefaultDispatcher-worker-1,5,main] 4 打印前的线程Thread[DefaultDispatcher-worker-2,5,main] 3 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
挂起函数
通过suspend来修饰的一个函数就是挂起函数。
挂起函数只能在协程代码内部调用,非协程代码不能调用。
suspend fun sayYourName() { delay(1000L) Log.d(TAG, "sayYourName : I am biumall") }
CoroutineScope(Dispatchers.IO).launch { sayYourName() }
如果不在协程中调用会有如下提示
Suspend function 'suspend fun sayYourName(): Unit' can only be called from a coroutine or another suspend function.主协程
主协程主要用runBlocking来表示,主协程有两种表达方式,一种方式是通过“:Unit=runBlocking”定义,另一种方式是通过“runBlocking{}”代码块的形式定义。
:Unit=runBlocking
使用:Unit=runBlocking定义主协程
//使用:Unit=runBlocking override fun onResume():Unit=runBlocking { super.onResume() sayYourName() }
也就是在函数后面添加了:Unit=runBlocking。
runBlocking{}
使用runBlocking{}定义主协程
override fun onResume() { super.onResume() //使用runBlocking runBlocking { sayYourName() } }
协程中的Job任务
协程可以通过launch()函数来启动,这个函数的返回值是一个Job类型的任务。Job类型的任务是协程创建的后台任务,它持有协程的引用,因此它代表当前协程的对象。获取Job类型的任务作用是判断当前任务的状态。
Job任务的状态有3种类型,分别是New(新建的任务)、Active(活动中的任务)、Completed(已结束的任务)。
Job任务中有两个字段,分别是isActive(是否在活动中)和isCompleted(是否停止),通过这两个字段的值可判断当前任务的状态。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) Log.d(TAG, "onCreate1 : ") val job: Job = CoroutineScope(Dispatchers.IO).launch { delay(1000L) Log.d(TAG, "onCreate2 : ") } Log.d(TAG, "onCreate3 主线程睡眠前:isActive=${job.isActive} isCompleted=${job.isCompleted}") Thread.sleep(2000L) Log.d(TAG, "onCreate4 主线程睡眠后:isActive=${job.isActive} isCompleted=${job.isCompleted}") }
打印日志
onCreate1 : onCreate3 主线程睡眠前:isActive=true isCompleted=false onCreate2 : onCreate4 主线程睡眠后:isActive=false isCompleted=true
###### 普通线程和守护线程
在Kotlin中的线程分为普通线程和守护线程。
普通线程是通过实现Thread类中的Runnable接口来创建的一个线程;
守护线程就是程序运行时在后台提供通用服务的一种线程,例如垃圾回收线程就是一个守护线程。
如果某个线程对象在启动之前调用了isDaemon属性并将其设置为true,则这个线程就变成了守护线程。
要将某个线程设置为守护线程,必须在该线程启动之前,也就是说isDaemon属性的设置必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。
线程与协程效率对比
一般情况下,线程有很多缺点,例如当启动一个线程的时候需要通过回调方式进行异步任务的回调,代码写起来比较麻烦。而且启动线程时占用的资源比较多,启动协程时占用的资源相对来说比较少。
协程取消
既然协程有启动,那么协程也就有取消。
在Kotlin中是通过cancel()方法将协程取消的。
val job: Job = CoroutineScope(Dispatchers.IO).launch { delay(1000L) Log.d(TAG, "launch : ") } //协程取消 job.cancel() //或者 //协程中的cancel()函数和join()函数是可以进行合并的,合并之后是一个cancelAndJoin()函数,这个函数用于取消协程
除了job.cancel()还有job.cancelAndJoin()
PS: job.cancelAndJoin() 要再挂起函数中
Suspend function 'suspend fun Job.cancelAndJoin(): Unit' can only be called from a coroutine or another suspend function.
协程取消失效
一般情况下,一个协程需要通过cancel()方法来取消,这种取消方式只适用于在协程代码中有挂起函数的程序。
由于挂起函数在挂起时也就是等待时,该协程已经回到了线程池中,等待时间结束之后会重新从线程池中恢复出来,虽然可以通过cancel()方法取消这些挂起函数,但是在协程中调用某些循环输出数据的函数时,通过cancel()方法是取消不了这个协程的。
fun cancelFail(): Unit = runBlocking { Log.d(TAG, "cancelFail : ") val job = launch(Dispatchers.IO) { // 程序运行时当前的时间 var nextTime = System.currentTimeMillis() while (true) { // 每一次循环的时间 val currentTime = System.currentTimeMillis() if (currentTime > nextTime) { Log.d(TAG, "cancelFail 当前时间:${System.currentTimeMillis()}") nextTime += 1000L } } } delay(2000L) // 使程序延迟2 秒 Log.d(TAG, "cancelFail 协程取消前:isActive=${job.isActive}") job.cancel() // 取消协程 job.join() // 执行完协程之后再执行后续操作 Log.d(TAG, "cancelFail 协程取消后:isActive=${job.isActive}") }
上面的就是取消失败,协程一直在打印。
可以通过下面几个方法解决取消失败:
通过对isActive值的判断来取消协程
如果想要在结束协程时结束协程中的循环操作,则需要在循环代码中通过isActive的值来判断当前协程的状态,如果isActive的值为false,则表示当前协程处于结束状态,此时返回当前协程即可。
if(!isActive) return@launch //返回当前协程使用yield()挂起函数来取消协程
还可以在循环代码中调用yield()挂起函数来结束协程中的循环操作,因为调用cancel()函数来结束协程时,yield()会抛出一个异常,这个异常的名称是Cancellation Exception,抛出这个异常之后协程中的循环操作就结束了,同时在循环代码中通过try…catch来捕获这个异常并打印异常名称,当捕获到这个异常之后将协程返回即可。
try { yield() } catch (e: CancellationException) { Log.d(TAG, "cancelFail e :$e ") return@launch }
有点跟Thread退出类似哈
定时取消
协程中可以通过withTimeout()函数来限制取消协程的时间。
fun forWithTimeout() { Log.d(TAG, "forWithTimeout: ") runBlocking { val job = CoroutineScope(Dispatchers.Default).launch { withTimeout(2000L) { Log.d(TAG, "forWithTimeout: ") repeat(1000) { i -> Log.d(TAG, "forWithTimeout I'm sleeping $i …") delay(500L) } } } job.join() } }
同步启动
fun callMethod(): Unit = runBlocking { val time = measureTimeMillis { //同步执行 val a = doJob1() val b = doJob2() Log.d(TAG, "callMethod : a=$a b=$b") } Log.d(TAG, "callMethod:执行时间=$time") } //挂起函数doJob1() suspend fun doJob1(): Int { Log.d(TAG, "doJob1: do") delay(1000L) Log.d(TAG, "doJob1: done") return 1 } //挂起函数doJob2() suspend fun doJob2(): Int { Log.d(TAG, "doJob2: do") delay(1000L) Log.d(TAG, "doJob1: done") return 2 }
日志打印
doJob1: do doJob1: done doJob2: do doJob1: done callMethod : a=1 b=2 callMethod:执行时间=2002
异步启动
异步启动通过async,跟上面差不多,修改点如下
fun callMethod(): Unit = runBlocking { val time = measureTimeMillis { //async执行 val a = async { doJob1() } val b = async { doJob2() } Log.d(TAG, "callMethod : a=$a b=$b") } Log.d(TAG, "callMethod:执行时间=$time") }
日志打印
callMethod : a=DeferredCoroutine{Active}@4b22740 b=DeferredCoroutine{Active}@3506679 callMethod:执行时间=1 doJob1: do doJob2: do doJob1: done doJob1: done
对比
| 方式 | 问题 |
|---|---|
| Thread + Handler | 易内存泄漏、难取消 |
| AsyncTask | 已废弃 |
| RxJava | 学习成本高、易过度设计 |
| 协程 | 简洁、可控、生命周期安全 |
参考文章
如果你愿意,我可以再帮你整理:
✅ 协程面试高频问答题
✅ suspend 编译后的状态机图解
✅ 协程 + Retrofit + Flow 标准写法模板
