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

相关推荐
帅次41 分钟前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛1 小时前
Android中Crash Debug技巧
android
kim56597 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼7 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ7 小时前
Android Studio使用c++编写
android·c++
csucoderlee7 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim56597 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式7 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。8 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio
ROCKY_8179 小时前
AndroidStudio-滚动视图ScrollView
android