Kotlin协程详解

协程调度器

协程调度器决定协程执行的线程,常见调度器有:

  • Dispatchers.Default:后台线程池,适合 CPU 密集型任务。

  • Dispatchers.IO:优化的线程池,适合 I/O 操作。

  • Dispatchers.Main:主线程,适合 UI 操作。

    使用示例

    val scope = CoroutineScope(Dispatchers.IO)
    scope.launch {
        // 执行网络请求等 I/O 操作
    }
    

协程作用域

协程作用域是协程执行的上下文环境,它定义了协程的生命周期,当作用域被取消时,其内部的协程也会被取消。常见的协程作用域有以下几种:

  • GlobalScope:全局作用域,生命周期与应用相同,不自动管理,易内存泄漏。

  • lifecycleScope:绑定 LifecycleOwner(Activity,Fragment),生命周期同步,自动取消内部协程。

  • viewModelScope:绑定 ViewModel,生命周期同步,自动取消内部协程。

  • CoroutineScope:自定义作用域,手动管理生命周期,需显式取消。

    使用示例

    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
        // 协程代码
        delay(2000)
        println("协程执行完毕")
    }
    // 取消作用域, 开启的协程也会取消, "协程执行完毕"不会打印
    scope.cancel()
    

协程

开启协程

通过协程作用域,可以使用 launchasync 构建器来创建协程。

  • launch

    launch 启动协程,返回 Job,无返回值。

    class MyActivity : AppCompatActivity() {
        fun startCoroutine() {
            val job1 = lifecycleScope.launch {
        		delay(2000)
        		println("协程1执行完毕")
            }
            val job2 = lifecycleScope.launch {
                job1.join() // 等待协程1执行完毕但没有返回值
        		println("协程2执行完毕")
            }
        }
    }
    
  • async

    async 启动协程,返回 Deferred,用于获取结果。

    class MyViewModel : ViewModel() {
        fun startCoroutine() {
            val deferred1 = viewModelScope.async {
        		delay(2000)
        		42 // 返回值
            }
            val deferred2 = viewModelScope.async {
           		val result = deferred1.await() // 等待协程1执行完成并获取结果
            	println("协程1执行结果:$result")
        		42 // 返回值
            }
        }
    }
    

    先打印“主线程继续执行”,然后等待协程执行完成,获取到返回值 42 后打印“协程执行结果:42”。

取消协程
  • cancel

    val scope = CoroutineScope(Dispatchers.Default)
    val job = scope.launch {
        // 协程代码
    }
    job.cancel() // 取消协程
    

子协程

作用域

在协程作用域内启动的协程称为子协程,子协程的生命周期受到父协程(即子协程作用域)的管理。

当父协程被取消时,其内部的所有子协程也会被取消。例如:

val scope = CoroutineScope(Dispatchers.Default)
val parentJob = scope.launch {
    // 开启子协程 1
    val childJob1 = launch {
        delay(2000)
        println("子协程 1 执行完毕")
    }
    // 开启子协程 2
    val childJob2 = launch {
        delay(3000)
        println("子协程 2 执行完毕")
    }
    cancel() // 取消父协程
}

当父协程执行到 cancel() 时,子协程 1 和子协程 2 也会被取消,不会继续执行。

挂起函数

挂起函数可以在执行过程中挂起,不会阻塞线程,当条件满足时恢复执行。挂起函数需要用 suspend 关键字修饰。

定义
  • 定义挂起函数

    suspend fun fetchData(): String {
        delay(1000) // 模拟耗时的网络请求
        return "数据"
    }
    
  • 在协程中调用挂起函数

    只能在协程或其他挂起函数中调用挂起函数。例如:

    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
        val data = fetchData()
        println("获取到的数据:$data")
    }
    

    调用 fetchData 时会挂起协程,延迟一秒后获取到数据并打印。

withContext
  • 基本用法

    withContext 在协程中切换调度器执行代码,完成后自动恢复原调度器,适用于不同线程操作。

    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
        delay(1000) // 模拟网络请求的延迟
        "获取到的数据"
    }
    

    调用 fetchData 时,先在 IO 线程请求数据,完成后自动返回原线程。

withTimeoutOrNull
  • 基本用法

    使用 withTimeoutwithTimeoutOrNull 可以设置协程超时时间,例如:

    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
        // 协程代码如果在 1000 毫秒内没有执行完成,会返回 null
        val result = withTimeoutOrNull(1000) {
            delay(2000)
            "结果"
        }
        if (result != null) {
            println("协程执行结果:$result")
        } else {
            println("协程超时")
        }
    }
    
awaitAll

如果有多个 Deferred 对象,可以使用 awaitAll 方法等待它们全部完成。例如:

GlobalScope.launch {
    val deferred1 = async { delay(1000); "结果 1" }
    val deferred2 = async { delay(1500); "结果 2" }
    val results = awaitAll(deferred1, deferred2)
    println("所有结果:${results.joinToString()}")
}

这里 awaitAll 会等待 deferred1deferred2 两个协程都执行完成,并返回一个包含所有结果的列表。

supervisorScope
  • 基本用法

    supervisorScope 让子协程独立执行,子协程失败不影响父协程。

    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
        supervisorScope {
            val child1 = launch {
                // 子协程 1
                delay(1000)
                throw Exception("子协程 1 异常")
            }
            val child2 = launch {
                // 子协程 2
                delay(2000)
                println("子协程 2 执行完毕")
            }
            child1.join()
            child2.join()
        }
        println("父协程执行完毕")
    }
    

    子协程 1 异常不影响子协程 2 和父协程的执行。

Flow

Flow 是一个用于处理异步数据流的协程构建器,它提供了丰富的操作符来处理和转换数据流。在工作开发中Flow也是协程使用最广泛的方式。

创建 Flow

使用 flow 构建器

使用 flow 构建器可以创建一个自定义的 Flow。在 flow 块中,可以使用 emit 函数发送数据。

fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(1000) // 模拟数据生成的延迟
        emit(i) // 发送数据
    }
}
使用 asFlow 扩展函数

可以将集合或数组转换为 Flow。

val numbers = listOf(1, 2, 3, 4, 5)
val numbersFlow = numbers.asFlow()

收集 Flow

使用 collect 方法可以收集 Flow 中的数据。collect 是一个挂起函数,必须在协程作用域内调用。

GlobalScope.launch {
    simpleFlow().collect { value ->
        println("接收到的数据:$value")
    }
}

数据转换

使用 map 操作符

可以使用 map 操作符对 Flow 中的每个数据项进行转换。

simpleFlow()
    .map { it * 2 } // 将每个值乘以 2
    .collect { value ->
        println("转换后的数据:$value")
    }
使用 filter 操作符

可以使用 filter 操作符过滤 Flow 中的数据。

simpleFlow()
    .filter { it > 2 } // 过滤出大于 2 的值
    .collect { value ->
        println("过滤后的数据:$value")
    }

异常处理

使用 catch 操作符

可以使用 catch 操作符捕获和处理 Flow 中的异常。

simpleFlow()
    .catch { e ->
        println("捕获到异常:${e.message}")
        emit(0) // 发送一个默认值
    }
    .collect { value ->
        println("接收到的数据:$value")
     }

合并多个 Flow

使用 combine 操作符

可以使用 combine 操作符合并多个 Flow,并对它们的最新值进行处理。

val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf("A", "B", "C", "D")
flow1.combine(flow2) { a, b ->
    "$a$b"
}.collect { value ->
    println("合并后的数据:$value")
}
// 打印
1 -> A
2 -> B
3 -> C
使用 zip 操作符

可以使用 zip 操作符将两个 Flow 中的值配对。

val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf("A", "B", "C", "D")
flow1.zip(flow2) { a, b ->
    "$a$b"
}.collect { value ->
    println("配对后的数据:$value")
}
// 打印
1 -> A
2 -> B
3 -> C
3 -> D

终止操作

使用 take 操作符

可以使用 take 操作符限制 Flow 中的数据项数量。

simpleFlow()
    .take(2) // 只取前两个数据项
    .collect { value ->
        println("接收到的数据:$value")
    }
使用 first 操作符

可以使用 first 操作符获取 Flow 中的第一个数据项。

GlobalScope.launch {
    val firstValue = simpleFlow().first()
    println("第一个数据项:$firstValue")
}

背压处理

背压(Back pressure)是指当数据的生产速度超过消费速度时,消费者对生产者产生的一种反馈压力。这种情况下,如果不加以处理,可能会导致数据堆积、内存溢出,甚至系统崩溃。

使用 buffer 操作符

通过缓冲区降低背压,保留所有数据,如果缓冲区满了,生产者会被暂停,直到消费者从缓冲区中取出数据。适用于需要减少背压影响,同时保留所有数据的场景。

simpleFlow()
    .buffer()
    .collect { value ->
        println("接收到的数据:$value")
        delay(2000) // 模拟耗时处理
    }
使用 conflate 操作符

conflate 实际上是 buffer 的一个特例,等同于 buffer(capacity = Channel.CONFLATED)。它不会保留所有数据,而是只保留最新的数据。这种方式可以显著减少处理频率,适合对最新数据更敏感的场景。

simpleFlow()
    .conflate()
    .collect { value ->
        println("接收到的数据:$value")
        delay(2000) // 模拟耗时处理
    }

与 LiveData 集成

可以将 Flow 转换为 LiveData,以便在 ViewModel 中使用。

class MyViewModel : ViewModel() {

    val usersLiveData: LiveData<List<User>> = flow {
        // 模拟从网络或数据库获取数据
        delay(2000)
        emit(listOf(User(1, "Alice", 30), User(2, "Bob", 25)))
    }.asLiveData(viewModelScope.coroutineContext)
}

源码分析

这里是用的装饰器模式,每一步操作符操作都是封装一个新的 Flow 对上一个 Flow 进行扩展,调用上一个 Flowcollect 方法传入 FlowCollector 数据收集器拿到数据流的 值,然后做相应的操作处理后,再调用 collectoremit 方法将值传出去,下一个操作符还是同样的操作。一直到最后一个操作符调用才会真正执行数据流下发的逻辑。

用java代码比较直观

public class MapFlow {
    private Flow preFlow;
    private MapFuction mapFun;

    public void collect(FlowCollector collector) {
        // 调用上一个Flow的collect方法,拿到数据流的值
        preFlow.collect(new FlowCollector() {
            public void emit(String s) {
                // map转换后将值传出去
                collector.emit(mapFun(s));
            }
        });
    }
}

高级用法

produce
  • 基本用法

    produce 可创建协程生产者,向 Channel 发送数据,其他协程可从 Channel 接收。

    val channel = GlobalScope.produce<Int>(capacity = Channel.UNLIMITED) {
        for (i in 1..5) {
            delay(1000) // 模拟数据生成的延迟
            send(i) // 将数据发送到 Channel
        }
    }
    GlobalScope.launch {
        for (value in channel) {
            println("接收到的数据:$value")
        }
        channel.cancel() // 取消生产者协程
    }
    

    这里 produce 创建了一个生产者协程,每隔一秒生成一个数字并发送到 Channel 中。另一个协程通过 for 循环从 Channel 中接收数据并打印。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值