在第二章对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 测试协程被取消了