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取数据

相关推荐
__water19 分钟前
RHA《Unity兼容AndroidStudio打Apk包》
android·unity·jdk·游戏引擎·sdk·打包·androidstudio
一起搞IT吧3 小时前
相机Camera日志实例分析之五:相机Camx【萌拍闪光灯后置拍照】单帧流程日志详解
android·图像处理·数码相机
浩浩乎@3 小时前
【openGLES】安卓端EGL的使用
android
Kotlin上海用户组4 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
zzq19965 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸5 小时前
Flutter 生命周期完全指南
android·flutter·ios
阿幸软件杂货间5 小时前
阿幸课堂随机点名
android·开发语言·javascript
没有了遇见5 小时前
Android 渐变色整理之功能实现<二>文字,背景,边框,进度条等
android
没有了遇见6 小时前
Android RecycleView 条目进入和滑出屏幕的渐变阴影效果
android
站在巨人肩膀上的码农7 小时前
去掉长按遥控器power键后提示关机、飞行模式的弹窗
android·安卓·rk·关机弹窗·power键·长按·飞行模式弹窗