Kotlin协程Flow与Channel对比

前言

kotlin 复制代码
fun main() {
    runBlocking {
        val flow = flow {
            emit("emit")
        }
        flow.collect{
            log("collect$it")
        }
    }
}

上游和下游属于同一个线程里。

  1. 操作符,即函数
  2. 上游,通过构造操作符创建
  3. 下游,通过末端操作符构建

只有下游才能通知上游放水,Flow属于冷流。生产数据的模块将生产过程封装到flow的上游里,最终创建了flow对象。

Channel核心原理与使用场景

Flow比较被动,在没有收集数据之前,上下游互不感知,管道并没有建立起来。

场景:需要将管道提前建立起来,在任何时候都可以在上游生产数据,在下游取数据,此时上下游可以感知的。

kotlin 复制代码
fun main() {
    // 提前建立通道/管道
    val channel = Channel<String>()
    GlobalScope.launch {
        // 上游放水
        delay(200)
        val data = "放水了"
        log("上游:$data")
        channel.send(data)
    }
    GlobalScope.launch{
        val data = channel.receive()
        log("下游收到:$data")
    }
    // 防止父线程过早退出
    Thread.sleep(250)
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]] 上游:放水了
[Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main]] 下游收到:放水了

先建立管道;往管道里放数据;从管道里取数据;

  1. 创建Channel
  2. 往Channel里放数据(生产)
  3. 从Channel里取数据(消费)

与Flow不同,生产者、消费者可以往Channel里存放/取出数据,只是能进行有效的存放,能否成功取出需要根据Channel状态确定。

Channel最大特点:

  1. 生产者、消费者访问Channel线程安全的,不管生产者和消费者在哪个线程,他们都能线程安全的存取数据
  2. 数据只能被消费一次,上游发送了一条数据,只要下游消费了数据,则其他下游将不会拿到此数据。

Flow切换线程的始末

场景:需要在flow里进行耗时操作(网络请求),外界拿到flow对象后等待收集数据即可。

kotlin 复制代码
fun main() {
    runBlocking { 
        val flow = flow { 
            thread { 
                Thread.sleep(3000)
                // 这里编译不过
                emit("emit")
            }
        }
    }
}

emit是挂起函数,需要在协程作用域里调用。

kotlin 复制代码
fun main() {
    runBlocking {
        val flow = flow {
            val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
            coroutineScope.launch {
                Thread.sleep(3000)
                // 这里运行报错:检测到在另一个线程里发射数据,这种行为不是线程安全的因此被禁止了
                emit("emit")
            }
        }
        flow.collect {
            log("collect:$it")
        }
    }
    // 防止父线程过早退出
    Thread.sleep(3500)
}
kotlin 复制代码
if (emissionParentJob !== collectJob) {
    error(
        "Flow invariant is violated:\n" +
                "\t\tEmission from another coroutine is detected.\n" +
                "\t\tChild of $emissionParentJob, expected child of $collectJob.\n" +
                "\t\tFlowCollector is not thread-safe and concurrent emissions are prohibited.\n" +
                "\t\tTo mitigate this restriction please use 'channelFlow' builder instead of 'flow'"
    )
}

会检测emit所在的协程与collect所在的协程是否一致,不一致则抛出异常。

ChannelFlow

既然是安全问题,那就封装一个

kotlin 复制代码
// 参数为SendChannel扩展函数
class MyFlow(private val block: suspend SendChannel<String>.() -> Unit) : Flow<String> {
    // 构造Channel
    private val channel = Channel<String>()
    override suspend fun collect(collector: FlowCollector<String>) {
        val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
        coroutineScope.launch {
            // 启动协程
            // 模拟耗时,在子线程执行
            Thread.sleep(3000)
            // 把Channel对象传递出去
            block(channel)
        }
        // 获取数据
        val data = channel.receive()
        // 发射
        collector.emit(data)
    }
}

重写了Flow的collect,当外界调用flow.collect时:

  1. 先启动一个协程
  2. 从channel里读取数据,没有数据则挂起当前协程
  3. 1里的协程执行,调用flow的闭包执行上游逻辑
  4. 拿到数据后进行发射,最终传递到collect的闭包

使用MyFlow:

kotlin 复制代码
fun main() {
    runBlocking {
        val myFlow = MyFlow{
            log("send")
            send("send")
        }
        myFlow.collect{
            log("collect")
        }
    }
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]] send
[Thread[Test worker @coroutine#1,5,main]] collect

上下游不在同一个协程里执行,也不在同一个线程里执行。

ChannelFlow核心原理

上面FLow没有使用泛型,没有对Channel进行关闭,不完善。

kotlin 复制代码
fun main() {
    runBlocking {
        val channelFlow = channelFlow {
            log("send")
            send("send")
        }
        channelFlow.collect{
            log("collect:$it")
        }
    }
}
输出:
[Thread[Test worker @coroutine#2,5,main]] send
[Thread[Test worker @coroutine#1,5,main]] collect:send

分析原理:

kotlin 复制代码
#ChannelFlow.kt
private open class ChannelFlowBuilder<T>(
    //闭包对象
    private val block: suspend ProducerScope<T>.() -> Unit,
    context: CoroutineContext = EmptyCoroutineContext,
    //Channel模式
    capacity: Int = Channel.BUFFERED,
    //Buffer满之后的处理方式,此处是挂起
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
) : ChannelFlow<T>(context, capacity, onBufferOverflow) {
    //...
    override suspend fun collectTo(scope: ProducerScope<T>) =
        //调用闭包
        block(scope)
    //...
}

public abstract class ChannelFlow<T>(
    // upstream context
    @JvmField public val context: CoroutineContext,
    // buffer capacity between upstream and downstream context
    @JvmField public val capacity: Int,
    // buffer overflow strategy
    @JvmField public val onBufferOverflow: BufferOverflow
) : FusibleFlow<T> {
    
    //produceImpl 开启的新协程会调用这
    internal val collectToFun: suspend (ProducerScope<T>) -> Unit
        get() = { collectTo(it) }
    
    public open fun produceImpl(scope: CoroutineScope): ReceiveChannel<T> =
        //创建Channel协程,返回Channel对象
        scope.produce(context, produceCapacity, onBufferOverflow, start = CoroutineStart.ATOMIC, block = collectToFun)

    //重写collect函数
    override suspend fun collect(collector: FlowCollector<T>): Unit =
        //开启协程
        coroutineScope {
            //发射数据
            collector.emitAll(produceImpl(this))
        }
}

produceImpl函数并不耗时,只是开启了新的协程。

kotlin 复制代码
#Channels.kt
private suspend fun <T> FlowCollector<T>.emitAllImpl(channel: ReceiveChannel<T>, consume: Boolean) {
    ensureActive()
    var cause: Throwable? = null
    try {
        //循环从Channel读取数据
        while (true) {
            //从Channel获取数据
            val result = run { channel.receiveCatching() }
            if (result.isClosed) {
                //如果Channel关闭了,也就是上游关闭了,则退出循环
                result.exceptionOrNull()?.let { throw it }
                break // returns normally when result.closeCause == null
            }
            //发射数据
            emit(result.getOrThrow())
        }
    } catch (e: Throwable) {
        cause = e
        throw e
    } finally {
        //关闭Channel
        if (consume) channel.cancelConsumed(cause)
    }
}

ChannelFlow应用场景

如:buffer、flowOn、flatMapLatest、flatMapMerge等

callbackFlow 原理

collect所在的协程为runBlocking协程,而send函数虽然在新的协程里,但他的协程调度器使用的是collect协程的,send函数和collect函数运行的线程是同一个线程。 虽然可以更改外层的调度器使运行在不同的线程,但不够灵活:

kotlin 复制代码
fun main() {
    GlobalScope.launch {
        val channelFlow = channelFlow {
            log("send")
            send("send")
        }
        channelFlow.collect{
            log("collect:$it")
        }
    }
    // 防止父线程过早退出
    Thread.sleep(100)
}
输出:
[Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]] send
[Thread[DefaultDispatcher-worker-2 @coroutine#1,5,main]] collect:send
kotlin 复制代码
fun main() {
    runBlocking {
        val channelFlow = channelFlow {
            getName(object :NetResult<String>{
                override fun onSuc(t: String) {
                    log("begin")
                    trySend("trySend")
                    log("end")
                }
                override fun onFail(err: String) {}
            })
        }
        channelFlow.collect{
            log("下游收到:$it")
        }
    }
    // 防止过早退出
    Thread.sleep(2500)
}

fun getName(callback:NetResult<String>){
    thread {
        // 模拟网络耗时
        Thread.sleep(2000)
        callback.onSuc("finish")
    }
}
interface NetResult<T>{
    fun onSuc(t:T)
    fun onFail(err:String)
}
输出:
[Thread[Thread-3,5,main]] begin
[Thread[Thread-3,5,main]] end

collect没有收到。getName函数内部开启了线程,它本身不是耗时操作,channelFlow闭包很快执行完成。CoroutineScope.produce的闭包执行结束后关闭channel。当子线程回调onSuc并执行trySend并不会往channel发送数据。

解决:不让协程关闭channel,只要协程没有结束,channel就不会关闭,在方法里调用挂起函数。

kotlin 复制代码
fun main() {
    runBlocking {
        val channelFlow = channelFlow {
            getName(object :NetResult<String>{
                override fun onSuc(t: String) {
                    log("begin")
                    trySend("trySend")
                    log("end")
                    // 关闭channel,触发awaitClose闭包执行
                    close()
                }
                override fun onFail(err: String) {}
            })
            awaitClose{
                // 走到此,channel关闭
                log("awaitClose")
            }
        }
        channelFlow.collect{
            log("下游收到:$it")
        }
    }
    // 防止过早退出
    Thread.sleep(2500)
}
输出:
[Thread[Thread-3,5,main]] begin
[Thread[Thread-3,5,main]] end
[Thread[Test worker @coroutine#1,5,main]] 下游收到:trySend
[Thread[Test worker @coroutine#2,5,main]] awaitClose
  1. awaitClose挂起协程,该协程不结束,则channel不关闭
  2. channel使用完成后需要释放资源,主动调用channel的close函数,该函数最终会触发awaitClose闭包执行,在闭包里做一些释放资源的操作。

callbackFlow

kotlin 复制代码
fun main() {
    runBlocking {
        val callbackFlow = callbackFlow {
            getName(object :NetResult<String>{
                override fun onSuc(t: String) {
                    log("begin")
                    trySend("trySend")
                    log("end")
                }
                override fun onFail(err: String) {}
            })
            awaitClose{
                // 走到此,channel关闭
                log("awaitClose")
            }
        }
        callbackFlow.collect{
            log("下游收到:$it")
        }
    }
}
输出:
[Thread[Thread-3,5,main]] begin
[Thread[Thread-3,5,main]] end
[Thread[Test worker @coroutine#1,5,main]] 下游收到:trySend

callbackFlow可以很好的使用。

Flow与Channel互转

Flow和Channel可以借助ChannelFlow互转。

kotlin 复制代码
fun main() {
    runBlocking {
        val channel = Channel<String> {  }
        val flow = channel.receiveAsFlow()
        GlobalScope.launch {
            flow.collect{
                log("collect:$it")
            }
        }
        delay(200)
        channel.send("send")
    }
    // 防止过早退出
    Thread.sleep(250)
}

channel通过send,flow通过collect收集

Flow 转 Channel

kotlin 复制代码
fun main() {
    runBlocking {
        val flow = flow {
            emit("emit")
        }
        val channel = flow.produceIn(this)
        val data = channel.receive()
        log("receive:$data")
    }
}

flow.produceIn(this)触发collect操作,进而执行flow闭包,emit将数据放到channel里,最后通过channel.receive取数据

相关推荐
恋猫de小郭10 分钟前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker5 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴5 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭15 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab16 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋1 天前
Android 协程时代,Handler 应该退休了吗?
android