Kotlin协程的介绍

352 阅读9分钟

一,什么是协程:

官方的介绍:

协程是一种并发设计模式,在Android平台上可以使用协程简化异步执行代码。(协程可以使我们是同步的代码形式实现异步任务的执行)

协程的特点:

  1. 轻量:一个线程上可以运行多个协程,协程支持挂起,协程的挂起不会阻塞运行协程的线程。挂起比阻塞更节省内存,也能让线程得到充分的利用。
  2. 内存泄漏更少:使用结构化并发在一个范围内(协程作用域)运行多项操作。
  3. 内置取消支持:取消通过正在运行的协程层次结构,自动传播。
  4. Jetpack集成:JetPack库提供了全面支持协程的扩展,提供了类似LifecycleScope,ViewModelScope绑定生命周期的作用域。

二,协程的使用:

1,添加相关依赖:

project的build.gradle中添加Kotlin编译插件

dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
}

app的build.gradle中添加协程相关的依赖库。

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}

2,构建启动一个协程

构建启动协程常用三种方式:

runBlocking:创建并启动一个新的线程,并且会阻塞当前的线程,直到协程内所有逻辑 和 子协程都执行完毕。项目中用不了,主要在@test测试代码中使用。

launch: 启动创建一个新的协程而不阻塞当前的线程。返回一个协程引用Job对象。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job 

context: 协程的上下文,协程的运行环境。可以是多个上下文组合,包括调度器(Dispatchers.Default)、协程本身的job、协程的名称(CoroutineName)、异常处理器(CoroutineExceptionHandler)

start: 协程的启动模式,立即启动 或 懒启动(Lazy)

block:是一个无参、无返回值的挂起函数,我们业务逻辑实现方法。

Job:协程的任务对象,可以通过Job 获取协程的状态和对协程执行操作。isActive()判断是否活跃,start()启动协程,cancel取消协程,join()挂起协程直到此job执行完成等,

fun test(){
    val job = MainScope().launch{
        Log.i("wang", "launch")
        delay(1000)//挂起函数
        Log.i("wang", "after one second")
        //两个launch构建并启动
        val job1= launch {
            delay(3000)
            Log.i("wang", "launch1")
        }
        launch {
            delay(3000)
            Log.i("wang", "launch2")
        }
        job1.join()//等待协程执行完,也是挂起函数,非阻塞的
        launch {
            delay(2000)
            Log.i("wang", "launch3")
        }
    }
    Log.i("wang", "end")
}

运行结果:

2025-04-28 09:53:32.449 14296-14296 wang                    I  end
2025-04-28 09:53:32.452 14296-14296 wang                    I  launch
2025-04-28 09:53:33.454 14296-14296 wang                    I  after one second
2025-04-28 09:53:36.467 14296-14296 wang                    I  launch1
2025-04-28 09:53:36.471 14296-14296 wang                    I  launch2
2025-04-28 09:53:38.480 14296-14296 wang                    I  launch3

async:启动创建一个新的协程而不阻塞当前的线程。返回一个协程引用Deffered对象。

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> 

block: 无参、有返回值T的挂起函数,较launch多了返回值。

Deffered: 是Job的子类,较Job多了个await()挂起函数,用于等待协程执行的结果返回值。

fun testAsync(){
    MainScope().launch {
        val deferred1 = async {
            delay(1000)
            Log.i("wang", "deferred1 execute")
            100
        }
        val deferred2 = async {
            delay(2000)
            Log.i("wang", "deferred2 execute")
            200
        }
        val deferred3 = async {
            delay(3000)
            Log.i("wang", "deferred3 execute")
            300
        }
        Log.i("wang", "sum = " + (deferred1.await() + deferred2.await() + deferred3.await()))
    }
}

运行结果:

2025-04-27 18:28:37.134  9747-9747  wang                    com.example.myapplication            I  deferred1 execute
2025-04-27 18:28:38.134  9747-9747  wang                    com.example.myapplication            I  deferred2 execute
2025-04-27 18:28:39.136  9747-9747  wang                    com.example.myapplication            I  deferred3 execute
2025-04-27 18:28:39.137  9747-9747  wang                    com.example.myapplication            I  sum = 600

3,协程的作用域

GlobalScope:作用域是全局,与应用程序的生命周期绑定。

coroutineScope: 创建一个独立协程作用域,所有的子协程都执行完了以后才结束自身,当前有一个子协程抛出异常后,所有未执行完成的子协程+自身都会停止执行。

superVisorScope: 同上类似,不同是子协程出现异常,不会影响父协程,也不会影响同阶的子协程。前提是你的子协程自己处理了异常(有CoroutineExceptionHandler)。

MainScope():上下文是SupervisorJob() + Dispatchers.Main。一个在主线程执行的协程作用域。

lifecycleScope:具有生命周期感知的协程作用域,与Lifecycle生命周期绑定,生命周期结束时,此作用域将被取消。

viewModelScope: 与lifecycleScope类似,与ViewModel的生命周期绑定。

MainScope().launch {
    try {
        coroutineScope {
            Log.i("wang" , "coroutineScope execute")
            launch {
                throw Exception(" coroutineScope 子协程失败")
            }
            launch {
                delay(100)
                Log.i("wang","因为其他子协程失败,这段代码不会被打印。")
            }
        }
    } catch (e: Exception) {
        Log.i("wang" , "捕获异常: ${e.message}")
    }
    try {
        supervisorScope {
            Log.i("wang" , "supervisorScope execute")
            launch(coroutineExceptionHandler()) {
                throw Exception("supervisorScope 子协程失败")
            }
            launch {
                delay(100)
                Log.i("wang","尽管其他子协程失败了,这段代码仍然会被打印。")
            }
        }
    } catch (e: Exception) {
        Log.i("wang" , "捕获异常: ${e.message}")
    }
}

输出结果如下:

18:25:17.253 wang     com.example.myapplication            I  coroutineScope execute
18:25:17.255 wang     com.example.myapplication            I  捕获异常:  coroutineScope 子协程失败
18:25:17.255 wang     com.example.myapplication            I  supervisorScope execute
18:25:17.255 wang     com.example.myapplication            I   exceptionHandler supervisorScope 子协程失败
18:25:17.356 wang     com.example.myapplication            I  尽管其他子协程失败了,这段代码仍然会被打印。

4,协程调度器

Kotlin 提供了四个调度器,您可以使用它们来指定应在何处运行协程:

调度器介绍
Dispatchers.Default默认调度器,非主线程。CPU密集型任务调度器。用于json解析,数据处理
Dispatchers.MainUI调度器, Andorid 上的主线程。用于UI更新
Dispatchers.Unconfined非指定线程的协程调度器,取决于启动的线程。
Dispatchers.IOIO调度器,非主线程,执行的线程是IO线程。用于网络请求,数据库处理、文件读写

5,suspend挂起函数

suspend:是kotlin的核心关键字,使用suspend关键字修饰的函数叫挂起函数,挂起函数只能在协程中或其他挂起函数中执行。

协程在执行到有suspend标记的挂起函数时,当前函数会被挂起(暂停),直到该挂起函数内部逻辑完成,才会在挂起的地方resume恢复继续执行,而不会阻塞运行协程的线程。

private fun testSuspend(){
    //启动了一个协程,去执行一个挂起函数
    Log.i("wang", "start")
    MainScope().launch {
        Log.i("wang", "start")
        val result = doWorkBackGroup()
        Log.i("wang", "result$result")
    }
}

private suspend fun doWorkBackGroup(): String{
    //模拟耗时任务
    delay(1000)
    Log.i("wang", "doWorkBackGroup end")
    return "hello world"
}

执行结果如下:

2025-04-28 17:34:21.049 27706-27706 wang                  I  start
2025-04-28 17:34:22.051 27706-27706 wang                  I  doWorkBackGroup end
2025-04-28 17:34:22.052 27706-27706 wang                  I  resulthello world

源码分析:KotlinBytecode后在deCompile后的代码如下,多了一个编译器加的参数Continuation,有个接口方法resumeWith(result), 即挂起函数执行完毕后恢复的回调。

private final Object doWorkBackGroup(Continuation $completion) {
@kotlin.SinceKotlin public interface Continuation<in T> {
    public abstract val context: kotlin.coroutines.CoroutineContext

    public abstract fun resumeWith(result: kotlin.Result<T>): kotlin.Unit
}

6,suspendCancellableCoroutine 函数的使用

在Android开发中将回调(Callback)改写成协程(Coroutine)的方式,会使的异步代码更简洁、易读。用例如下:


// 原始回调接口
interface Callback {
    fun onSuccess(result: String)
    fun onError(error: Throwable)
}

// 将回调改写成挂起函数,suspendCancellableCoroutine挂起当前函数等待回调,支持取消
suspend fun fetchData(): String = suspendCancellableCoroutine { continuation ->
    val callback = object : Callback {
        override fun onSuccess(result: String) {
            continuation.resume(result) // 成功时恢复协程
        }

        override fun onError(error: Throwable) {
            continuation.resumeWithException(error) // 失败时抛出异常
        }
    }

    // 调用原始异步方法
    performAsyncOperation(callback)

    // 处理协程取消(可选)
    continuation.invokeOnCancellation { 
        // 这里可取消原始操作(如网络请求),需要自己实现
        cancelAsyncOperation() 
    }
}
   
// 示例使用
viewModelScope.launch {
    try {
        val result = fetchData() // 同步写法
        showResult(result)
    } catch (e: Exception) {
        showError(e)
    }
}

总结具有如下优点:

  1. 消除回调地狱:异步代码线性执行
  2. 统一错误处理:使用 try/catch 捕获异常
  3. 生命周期安全:配合 viewModelScope/lifecycleScope 自动取消
  4. 取消传播:协程取消自动触发请求取消

注意事项:

  1. 确保每次回调 只调用一次 resume/resumeWithException
  2. 如果底层操作不支持取消,invokeOnCancellation 可省略
  3. 避免在回调中使用已取消的 continuation(通过 isActive 检查)

通过这种方式,你可以将任何基于回调的异步 API(网络请求、数据库操作、蓝牙交互等)无缝集成到 Kotlin 协程的同步编程模型中。

7,kotlin Flow的使用

Kotlin 协程库中异步流数据处理的响应式编程框架。基于协程实现的,提供了更简洁,更具kotlin风格的编程方式。

Flow核心特点:

  1. Flow 默认是冷流,但是StateFlow与SharedFlow是热流。
  2. 支持协程的取消和上下文传播
  3. 提供了丰富的操作符
  4. 通过协程挂起处理背压

1.1 冷流(cold-stream):每次收集(collect)时,启动新的数据流。 1.2.1,Flow的创建

//Flow的构建器
fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // 模拟异步操作
        emit(i)    // 发射值
    }
}

1.2.2,在协程作用域中接口数据

// 在协程作用域内收集
viewModelScope.launch {
    simpleFlow().collect { value -> 
        println(value) // 输出: 1, 2, 3 (间隔100ms)
    }
}

2.1 Flow的收集会响应协程的取消请求。(Flow的收集是在协程作用域中,emit函数的是挂起函数)。

// 示例:流收集的自动取消
val job = launch {
    flow {
        repeat(10) { i ->
            delay(500)
            emit(i)
            if (i == 3) cancel() // 测试取消
        }
    }.collect { println(it) }
}
// 输出: 0, 1, 2, 3 → 随后流被取消

3.1 中间操作符(返回新的flow):

.map { it * 2 }          // 转换值
.filter { it > 5 }       // 过滤
.transform { ... }       // 复杂转换
.take(3)                 // 取前N个
.flowOn(Dispatchers.IO)  // 指定上游上下文

3.2 终止操作符(启动数据流):

.collect { println(it) } // 收集值
.toList()                // 转为集合
.first()                 // 获取第一个值
.reduce { a, b -> a + b }// 聚合

3.3 异常处理:

.catch { e -> 
    println("Caught $e")
    emit(-1) // 异常时发射备用值
}
.onCompletion { cause ->   // 流完成时回调
    if (cause != null) println("Flow failed")
}

4,背压(backpressure)策略:Flow的生产者和消费者通过携程的挂起,来协调速度。

5.1 SharedFlow 与 StateFlow 对比

特性SharedFlowStateFlow
初始值不需要必须提供
值去重无自动去重自动去重(新值==旧值时不发射)
重放机制可配置 (0-N)固定重放1(当前状态)
使用场景事件(一次性通知)状态(持续存在的值)
值访问无法直接访问通过 .value 获取当前状态
状态保持不保持状态始终持有当前状态
典型应用事件总线、通知系统UI状态管理、配置管理

5.2 结合使用示例(事件 + 状态)

class AuthViewModel : ViewModel() {
    // 状态管理
    private val _authState = MutableStateFlow<AuthState>(AuthState.Idle)
    val authState: StateFlow<AuthState> = _authState.asStateFlow()
    
    // 事件管理
    private val _authEvents = MutableSharedFlow<AuthEvent>()
    val authEvents: SharedFlow<AuthEvent> = _authEvents.asSharedFlow()
    
    fun login(username: String, password: String) {
        viewModelScope.launch {
            _authState.value = AuthState.Loading
            try {
                val user = authRepo.login(username, password)
                _authState.value = AuthState.Authenticated(user)
                _authEvents.emit(AuthEvent.NavigateToHome)
            } catch (e: AuthException) {
                _authState.value = AuthState.Error(e)
                _authEvents.emit(AuthEvent.ShowErrorDialog(e))
            }
        }
    }
}