Kotlin(三) 协程

在第二章对kotlin实例化对象有一定了解后,由于下一章我们需要实现一个kotlin的网络库,所以这一章我们从协程开始吧.

前言

协程

优雅处理异步任务的解决方案,协程可以在不同的线程来回切换.这样可以让代码通过编写的顺序来执行,并且不会阻塞当前线程,省去了在各种耗时操作写回调的情况.

suspend

将当前线程暂时挂起,稍后自动切回来到原来的线程上.避免我们在主线程中调用耗时操作造成应用卡顿的情况

kotlin 复制代码
suspend fun doSth(sth: String):String{
    delay(2000)
    return "$sth sth finished"
}

协程常见操作符

  • runBlocking:运行阻塞:会阻塞当前线程执行
  • launch:不会阻塞当前线程,会异步执行代码
  • async:和launch相似,可以有返回值.

runBlocking

scss 复制代码
println("测试开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
runBlocking {
    println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    delay(2000)
    println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
}
println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
csharp 复制代码
2025-04-02 14:10:28.214 16048-16048 System.out              com.example.kotlinapplication        I  测试开始------------------true
2025-04-02 14:10:28.230 16048-16048 System.out              com.example.kotlinapplication        I  测试延迟开始------------------true
2025-04-02 14:10:30.233 16048-16048 System.out              com.example.kotlinapplication        I  测试延迟结束------------------true
2025-04-02 14:10:30.233 16048-16048 System.out              com.example.kotlinapplication        I  测试结束------------------true

runBlocking会阻塞当前的线程,只有等runBlocking里面的代码执行完了才会执行runBlocking外面的代码.

launch

scss 复制代码
println("测试开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
GlobalScope.launch {
    println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    delay(2000)
    println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
}
println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
csharp 复制代码
2025-04-02 14:59:05.276 17332-17332 System.out              com.example.kotlinapplication        I  测试开始------------------true
2025-04-02 14:59:05.293 17332-17332 System.out              com.example.kotlinapplication        I  测试结束------------------true
2025-04-02 14:59:05.294 17332-17371 System.out              com.example.kotlinapplication        I  测试延迟开始------------------false
2025-04-02 14:59:07.298 17332-17371 System.out              com.example.kotlinapplication        I  测试延迟结束------------------false

可以看到,GlobalScope.launch中变成了子线程,和runBlocking中的执行顺序不同,先打印测试开始结束后再执行lauch中的代码块内容

async

scss 复制代码
println("测试开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
GlobalScope.async {
    println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    delay(2000)
    println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
}
println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
csharp 复制代码
2025-04-02 15:04:10.522 17568-17568 System.out              com.example.kotlinapplication        I  测试开始------------------true
2025-04-02 15:04:10.539 17568-17568 System.out              com.example.kotlinapplication        I  测试结束------------------true
2025-04-02 15:04:10.540 17568-17611 System.out              com.example.kotlinapplication        I  测试延迟开始------------------false
2025-04-02 15:04:12.544 17568-17611 System.out              com.example.kotlinapplication        I  测试延迟结束------------------false

可以看到async的效果和launch一致.唯一的不同是async可以提供返回值

less 复制代码
println("测试开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))

lifecycleScope.launch {
    var async = GlobalScope.async {
        println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
        delay(2000)
        println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
        return@async "返回值"
    }
    println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    //由于await()方法需要指定在suspend方法里获得,这里我们使用lifecycleScope 对代码块进行包裹
    println("测试返回值------------------" + async.await())
}
csharp 复制代码
2025-04-02 15:19:42.149 18789-18789 System.out              com.example.kotlinapplication        I  测试开始------------------true
2025-04-02 15:19:42.156 18789-18789 System.out              com.example.kotlinapplication        I  测试结束------------------true
2025-04-02 15:19:42.157 18789-18828 System.out              com.example.kotlinapplication        I  测试延迟开始------------------false
2025-04-02 15:19:44.158 18789-18828 System.out              com.example.kotlinapplication        I  测试延迟结束------------------false
2025-04-02 15:19:44.159 18789-18789 System.out              com.example.kotlinapplication        I  测试返回值------------------返回值

可以看到,async可以通过await方法获取返回值.

线程调度器

  • Dispatchers.Main:在主线程中执行
  • Dispatchers.IO:在子线程中执行
  • Dispatchers.Default:默认调度器,没有设置调度器时就用这个
  • Dispatchers.Unconfined:无指定调度器,根据当前执行的环境而定,会在当前的线程上执行.

Dispatchers.Main

scss 复制代码
GlobalScope.launch(Dispatchers.Main) {
    println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    delay(2000)
    println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
}
println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
csharp 复制代码
2025-04-02 15:32:02.878 19263-19263 System.out              com.example.kotlinapplication        I  测试开始------------------true
2025-04-02 15:32:02.895 19263-19263 System.out              com.example.kotlinapplication        I  测试结束------------------true
2025-04-02 15:32:03.061 19263-19263 System.out              com.example.kotlinapplication        I  测试延迟开始------------------true
2025-04-02 15:32:05.064 19263-19263 System.out              com.example.kotlinapplication        I  测试延迟结束------------------true

区别于之前的未指定线程,这里面launch代码块里仍然执行的是在主线程中

  • 如果没有指定launch语句的调度器,在上面的代码中是在子线程中执行的,但是我们在代码中强制指定了Dispatchers.Main为主线程

withContext(协程内部线程调度)

scss 复制代码
println("测试开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))

GlobalScope.launch(Dispatchers.Main) {
    withContext (Dispatchers.IO){
        println("测试是否为主线程------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    }
    println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    delay(2000)
    println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
}
println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
csharp 复制代码
2025-04-02 15:42:05.574 19825-19825 System.out              com.example.kotlinapplication        I  测试开始------------------true
2025-04-02 15:42:05.592 19825-19825 System.out              com.example.kotlinapplication        I  测试结束------------------true
2025-04-02 15:42:05.730 19825-19881 System.out              com.example.kotlinapplication        I  测试是否为主线程------------------false
2025-04-02 15:42:05.761 19825-19825 System.out              com.example.kotlinapplication        I  测试延迟开始------------------true
2025-04-02 15:42:07.764 19825-19825 System.out              com.example.kotlinapplication        I  测试延迟结束------------------true

withContext的作用是将当前线程挂起,只有当withContext里面的代码执行完了,才会恢复当前线程的执行.

withContext的执行方式如下:

kotlin 复制代码
suspend fun getUserName(userId: Int):String{
    return withContext(Dispatchers.IO) {  
        delay(2000)
        return@withContext "TestName--getUserName"
    }
}

suspend fun getUserName2(userId: Int):String=withContext (Dispatchers.IO){
    delay(2000)
    return@withContext "TestName--getUserName2"
}

协程的启动模式

  • CoroutineStart.DEFAULT:默认模式,会立即执行
  • CoroutineStart.LAZY:懒加载模式,不会执行,只有手动调用协程的start方法才会执行
  • CoroutineStart.ATOMIC:原子模式,跟CoroutineStart.DEFAULT类似,但协程在开始执行之前不能被取消。
  • CoroutineStart.UNDISPATCHED:未指定模式,会立即执行协程
scss 复制代码
println("测试开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))

val job=GlobalScope.launch(Dispatchers.Main, CoroutineStart.LAZY) {
    withContext (Dispatchers.IO){
        println("测试是否为主线程------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    }
    println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
    delay(2000)
    println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
}
println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
job.start()

协程启动方法

  • job.start:启动协程,除了lazy模式,协程都不需要手动启动
  • job.cancel:取消一个协程,可以取消,但是不会立马生效,存在一定延迟
  • job.join:等待协程执行完毕,这是一个耗时操作,需要在协程中使用
  • job.cancelAndJoin:等待协程执行完毕然后再取消

协程超时

withTimeout

我们可以在协程执行的时候,给它设置一个执行时间,如果执行的耗时时间超过规定的时间,那么协程就会自动停止.

scss 复制代码
GlobalScope.launch() {
    try {
        withTimeout (300){
            //重复执行5次里面的内容
            repeat (5){
                i->
                println("测试输出 $i")
                delay(100)
            }
        }
    }catch (e: TimeoutCancellationException){
        println("测试协程超时了.................")
    }
}
csharp 复制代码
2025-04-02 16:00:25.874 20873-20915 System.out              com.example.kotlinapplication        I  测试输出 0
2025-04-02 16:00:25.977 20873-20915 System.out              com.example.kotlinapplication        I  测试输出 1
2025-04-02 16:00:26.078 20873-20915 System.out              com.example.kotlinapplication        I  测试输出 2
2025-04-02 16:00:26.174 20873-20915 System.out              com.example.kotlinapplication        I  测试协程超时了.................

可以看到,在已有的程序逻辑里,当达到超时时间时,程序会主动抛出异常.在网络请求超时的设计思路中,我们或许可以采用这种方式.

withTimeoutOrNull

scss 复制代码
val result= withTimeoutOrNull (300){
    //重复执行5次里面的内容
    repeat(5) {
        i->
        println("测试输出---------------"+i)
        delay(100)
    }
    return@withTimeoutOrNull "执行完成-------------"
}

println("测试输出结果---------------"+result)
csharp 复制代码
2025-04-02 16:08:20.330 21424-21465 System.out              com.example.kotlinapplication        I  测试输出---------------0
2025-04-02 16:08:20.430 21424-21465 System.out              com.example.kotlinapplication        I  测试输出---------------1
2025-04-02 16:08:20.531 21424-21465 System.out              com.example.kotlinapplication        I  测试输出---------------2
2025-04-02 16:08:20.630 21424-21465 System.out              com.example.kotlinapplication        I  测试输出结果---------------null

withTimeoutOrNull在超时后不会主动抛出异常,而是直接返回null,供我们在代码中做出判断.

scss 复制代码
val result= withTimeoutOrNull (1000){
    //重复执行5次里面的内容
    repeat(5) {
        i->
        println("测试输出---------------"+i)
        delay(100)
    }
    return@withTimeoutOrNull "执行完成-------------"
}

println("测试输出结果---------------"+result)

此时我们将代码中的超时时间稍微设置长一点,程序即可正常返回.

csharp 复制代码
2025-04-02 16:13:01.081 21833-21871 System.out              com.example.kotlinapplication        I  测试输出---------------0
2025-04-02 16:13:01.182 21833-21871 System.out              com.example.kotlinapplication        I  测试输出---------------1
2025-04-02 16:13:01.282 21833-21871 System.out              com.example.kotlinapplication        I  测试输出---------------2
2025-04-02 16:13:01.383 21833-21871 System.out              com.example.kotlinapplication        I  测试输出---------------3
2025-04-02 16:13:01.484 21833-21871 System.out              com.example.kotlinapplication        I  测试输出---------------4
2025-04-02 16:13:01.585 21833-21871 System.out              com.example.kotlinapplication        I  测试输出结果---------------执行完成-------------

lifecycleScope

如果我们在代码中直接使用GlobalScope类,是会有黄色的警告的.

GlobalScope中启动的写成不受结构化并发原则约束,所以如果它挂起或由于问题(例如由于网络缓慢)而延迟,它将继续工作并消耗资源.

由于它开启的协程是全局的,是不会跟随组件(例如Activity)的生命周期,这样就可能会导致一些内存泄漏的问题

lifecycleScope

kotlin 复制代码
fun test(){
    lifecycleScope.launch { 
        
    }
}

这样协程会在Lifecycle派发destroy事件的时候cancel掉

viewModelScope

kotlin 复制代码
fun test(){
    viewModelScope.launch {
        
    }
}

协程会在ViewModel调用clear方法的时候cancel掉

cancel

如果我们使用了GlobalScope,又担心内存泄漏,此时可以手动调用cancel()确保协程已取消.

ini 复制代码
val launch=GlobalScope.launch {

}
//手动取消
launch.cancel()
scss 复制代码
println("测试开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))

val job=GlobalScope.launch() {
    try {

        println("测试延迟开始------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
        delay(20000)
        println("测试延迟结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
        delay(20000)
        println("测试延迟结束")
    }catch (e: CancellationException){
        println("测试协程被取消了")
    }

}
println("测试结束------------------" + (Thread.currentThread() == Looper.getMainLooper().thread))
job.cancel()
csharp 复制代码
2025-04-02 16:41:53.285 23850-23850 System.out              com.example.kotlinapplication        I  测试开始------------------true
2025-04-02 16:41:53.301 23850-23850 System.out              com.example.kotlinapplication        I  测试结束------------------true
2025-04-02 16:41:53.302 23850-23891 System.out              com.example.kotlinapplication        I  测试延迟开始------------------false
2025-04-02 16:41:53.306 23850-23891 System.out              com.example.kotlinapplication        I  测试协程被取消了
相关推荐
姝然_95272 分钟前
cursor vue3 rules
前端
littleplayer2 分钟前
iOS 中的 @MainActor 详解
前端·swiftui·swift
嘻嘻嘻嘻嘻嘻ys7 分钟前
《智能编码新纪元:GitHub Copilot+Cursor实战开发效能跃迁指南》
前端
zhangxiao9 分钟前
预览组件 支持图片跟PDF
前端
LAOLONG-C35 分钟前
今日CSS学习浮动->定位
前端·css·css3
城南旧事41 分钟前
SSE (Server-Send Events) 服务端实时推送技术
前端
Mapmost1 小时前
【数据可视化艺术·应用篇】三维管线分析如何重构城市"生命线"管理?
前端·数据可视化
palpitation972 小时前
在Flutter中使用Builder的正确方式:一场context的教育
前端
Eliauk__2 小时前
深入剖析 Vue 双向数据绑定机制 —— 从响应式原理到 v-model 实现全解析
前端·javascript·面试
代码小学僧2 小时前
Cursor 的系统级提示词被大佬逆向出来了!一起来看看优秀 prompt是怎么写的
前端·ai编程·cursor