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  测试协程被取消了
相关推荐
庸俗今天不摸鱼18 分钟前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下25 分钟前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox36 分钟前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞39 分钟前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行39 分钟前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_5937581040 分钟前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox
掘金一周43 分钟前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
三翼鸟数字化技术团队1 小时前
Vue自定义指令最佳实践教程
前端·vue.js
Jasmin Tin Wei2 小时前
蓝桥杯 web 学海无涯(axios、ecahrts)版本二
前端·蓝桥杯
圈圈编码2 小时前
Spring Task 定时任务
java·前端·spring