「最后一次,彻底搞懂kotlin Flow」(二) | 庖丁解流: Flow

971 阅读16分钟


引言

  上一篇我们通过把 Flow 与类似概念的对比来为 Flow 下了一个定义,认识了 Flow 的五项全能属性。接下来我们就来从结构的角度来更加深入的认识 Flow,Flow 从结构来看,可以分为三个部分,Builder,Operator,Collector,对应于 Flow 的创建,处理,和收集,下面我们一一来认识一下。

解构 Flow

Builder

Standard

  我们先来看看最简单的例子,看看一个标准的 Flow 是如何构建的:

// FlowDeepDive1.kt
fun main () {
    // 1. builder
    val flow = flow {
        emit(1)
        emit(2)
        emit(3)
    }

    // 2. collect
    runBlocking {
        flow.collect { println(it) }
    }
}

 // log
1
2
3

  这是构建 Flow 最底层的方法,其内部构造了一个 SafeFlow 对象

public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)

  接下来我们看看几种代表性的基于此的衍生方法。

Iterable.asFlow

  Collection 接口实现了 Iterable 接口,其下面的接口如 List,Set, Queue,Dequeue 等实现了 Collection 接口,所以这些接口的实现都可以直接转换成 Flow:

// FlowDeepDive3.kt
fun main() {
    // 1. builder
    val flow = listOf(1,2,3).asFlow()

    // 2. collect
    runBlocking {
        flow.collect { println(it) }
    }
}


  Map 也可以通过内部的方法间接转换为 Flow:

// FlowDeepDive4.kt
fun main() {
    val map = mapOf("1" to 1, "2" to 2, "3" to 3)

    // 1. builder
    val valuesFlow = map.values.asFlow()
    val keysFlow = map.keys.asFlow()
    val entriesFlow = map.entries.asFlow()

    // 2. collect
    runBlocking {
        valuesFlow.collect { println(it) }
        keysFlow.collect { println(it) }
        entriesFlow.collect { println(it) }
    }
}


  这样,我们的 Collection 和 Map 容器都可以轻松的转换为 Flow,并使用 Flow 的非阻塞式异步流的好处。 asFlow 内部的实现非常简单:

public fun <T> Iterable<T>.asFlow(): Flow<T> = flow {
    forEach { value ->
        emit(value)
    }
}


  就是用我们前面的 standard flow builder,在内部用 forEach 遍历 ,然后 emit 每一个 item,就这么简单。

Sequence.flow

  除了 Collection 和 Map,我们上一篇提到的同样是懒计算的 sequence 也可以转换为 Flow:

// FlowDeepDive5.kt
fun main() {
    // 1. builder
    val flow = sequenceOf(1, 2, 3).asFlow()

    // 2. collect
    runBlocking {
        flow.collect { println(it) }
    }
}


  内部的实现跟 Iterable.asFlow 的如出一辙,但 Sequence 本身并未实现 Iterable 接口,内部重载了 iterator operator,所以可以通过 for 循环来读取 item,然后再 emit:

public fun <T> Sequence<T>.asFlow(): Flow<T> = flow {
    forEach { value ->
        emit(value)
    }
}

毕竟 sequence 和 List 从接口上相似度很高,我们再来看一个不太一样的。

Suspend <-> flow

  没错,suspend 方法也能转化为 Flow,来看看这个例子,其思想就是把 suspend 的返回值塞到 flow 当中,所以这样生成的 flow 只有一个值:

// FlowDeepDive6.kt
fun main() {
    runBlocking {
        // 1. asFLow
        val flow = ::getAccount.asFlow()
        
        // 2. collect
        flow.onStart { printlnWithTime("start") }
            .collect { printlnWithTime(it) }
    }
}

suspend fun getAccount(): Int {
    delay(3000)
    return 1
}

// log
3350: start
6361: 1


  在 asFlow 注释这里通过对 suspend 方法 getAccount 的引用调用 asFlow 方法把方法转化成了 flow,在这个 flow collect 3s 后收集到了数据。来看看内部实现:

public fun <T> (suspend () -> T).asFlow(): Flow<T> = flow {
    emit(invoke())
}

不出所料,仍然是使用 standard flow build,在 builder 内部调用了这个方法,然后把值 emit 出去了。

  细心的小伙伴会发现这小节的标题用的是 <-> 连接 flow 和 suspend,flow 也可以转换为 suspend?没错,不过官方没有这个方法,我们自己来实现看看:

// FlowDeepDive7.kt
fun main() {
    // 1. build flow
    val flow = flowOf(1, 2, 3)
    // 2. flow to suspend
    val suspendFunc = flow.asSuspend()

    // 3. call suspend
    val intList = runBlocking {
        suspendFunc()
    }
    println(intList)
}

fun <T> Flow<T>.asSuspend() = suspend { toList() }

// log
[1, 2, 3]


  asSuspend 方法出乎意料的简单,就是调用了一次 suspend 方法,然后内部调用了 toList。因为 suspend 方法只能返回一个值,所以需要把 flow 流里面的所有值压缩到一个容器汇中,这里我们直接调用 toList。


  看来复杂的东西在这个 suspend 方法里,我们来看看 suspend 方法:

// buildin Suspend.kt
public inline fun <R> suspend(noinline block: suspend () -> R): suspend () -> R = block

  居然如此简单,就是把传入的 suspend block 再返回了一次。没错,最重要的就是我们把对于 toList 方法的调用封装进了一个 suspend block,这个 block 就是我们得到的 suspend 方法了。下面我们再介绍一种重要的构建 flow 的方式。

callbackFlow

  我们还是直接先看示例,我们用这个实例来模拟用户点击事件:

// FlowDeepDive8.kt
class View {
    private var onClick: ((View) -> Unit)? = null

    fun mockUserClick() = onClick?.invoke(this)

    fun onClick(onClick: (View) -> Unit) {
        this.onClick = onClick
    }

    fun removeClick() {
        this.onClick = null
        println("removeClick")
    }
}

private fun click(view: View) = repeat(3) { view.mockUserClick() }


  首先我们定义一个简单的 View,包含 onClick,removeClick 方法,以及一个用于模拟用户点击的 mockUserClick 方法,以及一个用于连续点击 3 次的辅助方法 click,然后我们使用这个这个类,再调用 click 来模拟连续点击3 次:

// FlowDeepDive8.kt
private fun callbackSample() {
    val view = View()
    view.onClick { println("View click from callback") }
    click(view)
}

// log
View click from callback
View click from callback
View click from callback


  log 非常简单,接下来我们把这个 callback 转换为一个 flow,通过对 flow 的 collect 来监听点击事件:

// FlowDeepDive8.kt
private fun callbackFlowSample() {
    // 1. view
    val view = View()

    // 2. callbackFlow
    val callbackFlow = callbackFlow {
        // 2.1 register callback
        view.onClick {
            launch { send(view) }
        }
        // 2.2 unregister callback
        // will be auto call when cancel flow collect
        awaitClose { view.removeClick() }
    }

    // 3. run
    runBlocking {
        // 3.1 collect
        val job = launch {
            callbackFlow.collect { println("View click from flow") }
        }
        delay(100)

        // 3.2 mock user click
        click(view)
        delay(100)

        // 3.3 cancel flow collect
        job.cancel()
    }
}

// log
View click from flow
View click from flow
View click from flow
removeClick


  这段代码稍微长点,分为3个部分,但不复杂。 part1 创建 view,part2 就是 callback 转 flow 的核心,2.1 是 register callback,只是这里不再直接消费事件,而是通过调用 send 把 event 往 flow 的下游转发,2.2 会在 flow 的 collect 被 cancel 的时候自动调用,这里我们 remove click 监听,这样我们就把 callback 转换成了 flow。可是,事情好像变得更加复杂了,比起 callbackSample 3 行代码搞定,我们 part2 中的代码更多也更复杂了些。我们付出了复杂度的代价,那么我们的收获是什么呢?下面我们就进入操作符部分,我们会介绍一些典型的操作符,在学习完 operator 后在最后的练习部分体会 callback 转 flow 带来的好处。

Operator

map

  我们从最熟悉的 map 开始,以 map 为起点深入来看看 flow 的操作符实现原理:

// FlowDeepDive9.kt
fun main() = runBlocking {
    flowOf(1, 2, 3)
        .map { it * it }
        .collect { println(it) }
}

// log
1
4
9


  上面这段代码非常简单,下面我们来看看 map 的内部:

// 1. map
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
    return@transform emit(transform(value))
}

// 2. transform
internal inline fun <T, R> Flow<T>.unsafeTransform(
    @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R> = unsafeFlow {
    collect { value ->
        return@collect transform(value)
    }
}

// 3. unsafeFlow
internal inline fun <T> unsafeFlow(@BuilderInference crossinline block: suspend FlowCollector<T>.() -> Unit): Flow<T> {
    return object : Flow<T> {
        override suspend fun collect(collector: FlowCollector<T>) {
            collector.block()
        }
    }
}


  map 内部调用了 transform,transform 调用了 unsafeFlow,这个 unsafeFlow 返回了一个 Flow 对象,跟前面我们提到的 SafeFlow 对象类似,所以这个 unsafeFlow 方法就是 standard flow builder 的一个 unsafe 版本。如果我们把上面的方法 inline 一下,map 方法大概就会变成这样:

public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = unsafeFlow {
    // collect
    collect { value ->
        emit(transform(value))
    }
}


  上面 collect 的是外层的扩展方法的 receiver 的 Flow,并不是新创建的 unsafeFlow,所以 map 方法所做的就是:

  1. create 一个 unsafeFlow
  2. 返回这个 unsafeFlow
  3. 当这个 unsafeFlow 被 collect 时,内部的 block 会被执行,即 collect 上游的 flow,再传递到 unsafeFlow 的 collector

Flow 绝大多数的操作符都是通过 transform 实现的,知道了这个实现方式之后,我们再来看看其他的操作符。

filter

  来看看 filter 系列的例子

// FlowDeepDive10.kt
fun main() = runBlocking {
    flowOf(1, 2, 3, 4, null, 5, 6, 7)
        .filterNotNull() // 1,2,3,4,5,6,7
        .filter { it % 2 != 0 } // 1,3,5,7
        .filterNot { it % 3 == 0 } // 1,5,7
        .drop(1) // 5,7
        .take(1) // 5
        .collect { println(it) } // 5
}

//log
5


  上面这些这些都是 filter 类的操作符,filterNotNull 过滤了 null 值,filter 把 符合条件的值往下游传递,对应于白名单操作,filterNot 把符合条件的值给过滤掉了,对应于黑名单操作,drop 过滤了 指定数量的值,take 过滤了指定数量外的值,drop 和 take 分别都有带有条件的运算符版本,这里不再展开,下面我们就从 filter 入手来看看 filter 系列的操作符如何实现:

// filter
public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
    if (predicate(value)) return@transform emit(value)
}


  没错,就是这么简单,符合条件的就往下传递,不符合的自然就被过滤掉了,其他的操作符相信大家都明白是如何实现的了,下面我们来看看稍微复杂一点的操作符。

merge

  顾名思义,就是把多个 flow 合并成一个 flow,注意 merge 是一个顶层方法,而不是一个 flow 操作符:

// FlowDeepDive11.kt
fun main() = runBlocking {
    val flow1 = flowOf(1, 2, 3)
    val flow2 = flowOf('A', 'B', 'C')
        .map {
            delay(100)
            it
        }
    val flow3 = flowOf("一", "二", "三")

    merge(flow1, flow2, flow3)
        .collect { println(it) }
}

// log
123一二三ABC


  从 log 可以看出来,flow1 的 item 先被打印出来,然后是 flow3 的,再是 flow2 的,看来 map 里面的 delay 起到了效果。我们也可以推断出来 flow 之间并不是严格按照顺序来的,而是各凭本事,谁发得快谁就先被 merge 后的 flow collect 到。我们来看看 merge 的核心代码:

// Merge.kt
override suspend fun collectTo(scope: ProducerScope<T>) {
    val collector = SendingCollector(scope)
    flows.forEach { flow ->
        scope.launch { flow.collect(collector) }
    }
}

在 collectTo 方法中,遍历了传入的 flow,然后再分别 launch 了一个 collect 的 Coroutine,这样三个之间就没有严格的顺序关系了,三个 flow 的数据都被 collect 到一个 collector 中。要想保持顺序该怎么处理呢?很简单,去掉 scope.launch。下面我们来看另外一种"合并"的操作符。

zip

fun main() = runBlocking {
    val flow1 = flowOf(1, 2, 3, 4)
    val flow2 = flowOf('A', 'B', 'C')

    flow1
        .zip(flow2) { v1, v2 ->
            "$v1:$v2"
        }
        .collect { println(it) }
}

// log
1:A
2:B
3:C


  如何理解 zip 呢?想想 zip 的翻译就会恍然大悟:拉链。就像衣服的拉链一样,zip 会把左边和右边的 flow 的 item 一一对应上,直到拉链把一边的拉链齿用完,就如同其中一个 flow 的数据发射完毕。上面 flow1 中的 item 4 就会因为没有匹配的拉链齿而无法配对。我们来看看 zip 的关键实现:

zip.webp

zip


// zipImpl
flow.collect { value ->
    // 1. collect a value from flow
    withContextUndispatched(scopeContext, Unit, cnt) {
      	// 2. collect a value from second flow
        val otherValue = second.receiveCatching().getOrElse {
            throw it ?: AbortFlowException(collectJob)
        }
      	// 3. transform and emit combined value
        emit(transform(value, NULL.unbox(otherValue)))
    }
}


  part1 首先从第一个 flow collect 一个值,然后从第二个 flow collect 一个值,再“zip”两个值并发送。直到 flow collect 结束,或者 第二个 flow 结束通过异常结束 flow 的 collect。核心实现总是如此的简单,下面我们再来看一类操作符,。

  这类操作符用于把 flow 转换为普通值的操作符,这种把 flow 转换为普通值的操作符咋听起来像是 collect 做的事,但其实并不是。这里的操作符是用于在收集 flow 时 ,flow 的 item 会通过某种方式转变为 flow 的情况。即初始为 flow<R>, 然后在 flow 内部发生 R -> flow<T> 的转换,这种转换会使数据结构变成 flow<flow<T>>。这种结构并不方便直接处理 T,通常这种结构并不是我们想要的,我们想要的是 flow<T>,所以我们就需要把内部flow<T> 转变为 T,并通过一个外部的 flow 发送出来,这样我们就能得到 flow<T>,下面我们介绍两种相关操作符。

flatMapMerge

// FlowDeepDive13.kt
fun main() = runBlocking {
    getTopics().flatMapMerge { getArticlesFromTopic(it) }
        .collect { printlnWithTime(it) }
}

fun getTopics(): Flow<String> = flowOf("A", "B", "C")

fun getArticlesFromTopic(topic: String): Flow<String> = flow {
    repeat(3) {
        delay(100) // mock network delay
        emit("$topic: $it")
    }
}

// log
5899: A: 0
5899: B: 0
5899: C: 0
6004: A: 1
6004: B: 1
6004: C: 1
6106: A: 2
6106: B: 2
6107: C: 2

  从 log 可以看出,这里的 flatMapMerge 的策略跟上面的 merge 类似,都是各凭能力,哪个 flow 发得快,下面就收的快,没有强制的顺序关系。其内部实现跟 merge 思路一致,只是里面多了 semaphore 并发控制,下来可以去看看源码。下面我们介绍另外一种

flatMapConcat

// FlowDeepDive14.kt
fun main() = runBlocking {
    getTopics().flatMapConcat { getArticlesFromTopic(it) }
        .collect { printlnWithTime(it) }
}

// log
3480: A: 0
3585: A: 1
3690: A: 2
3796: B: 0
3901: B: 1
4007: B: 2
4111: C: 0
4217: C: 1
4321: C: 2


  从上面 log 可以看出,flatMapConcat 是会把一个 flow 完全 emit 完毕才会开始下一个 flow 的 emit,我们来看看内部实现:

public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T> = flow {
    collect { value -> emitAll(value) }
}


  就是依次 collect 每一个 flow,再通过 emitAll 通过一个 flow 发射出去,就是如此的简介,也确实印证了我们前面提到的如何把 merge 变为按 flow 的顺序依次 collect 的。再注意看看方法的声明 Flow<Flow<T>>.flattenConcat(): Flow<T> ,这跟我们前面的对于 flat 系列的任务的描述一致。

实验

  下面我们做一个有趣的实验,如果我们要把上面 FlowDeepDive11.kt 的例子改造一下,变成按照 flow 的顺序依次 collect,你会如何改造?先想一想,然后我们来看看这个有趣的例子:

// FlowDeepDive15.kt
fun main() = runBlocking {
    flowOf(::getFlow1, ::getFlow2, ::getFlow3) // Flow<Flow<R>>
        .flatMapConcat { it() } // Flow<R> -> T : Flow<T>
        .collect { print(it) } // T
}

private fun getFlow1(): Flow<Int> = flowOf(1, 2, 3)

private fun getFlow2() = flowOf('A', 'B', 'C')
    .map {
        delay(100)
        it
    }

private fun getFlow3() = flowOf("一", "二", "三")

// log
123ABC一二三


  从上面的 log 可以看出,我们的 flow 现在严格按照顺序 collect 了。 在上面的示例中,我们把 flow1,2,3 改造成可以获得 flow 的方法,然后通过 flowOf 构造对这些方法的引用,这就是 Flow<R>,然后我们通过 flatMapConcat 把 Flow<R> 变成了 Flow<Flow<T>>,再到 Flow<T>,你是如何实现的呢?前面我们看了不少用于数据类型转换的操作符,下面我们再来看看一下 collect 方法的变种。

collect

  collect 本身大家应该很熟悉了,我们来看看一些特殊的 collect,collect 某个特殊的值:

// FlowDeepDive16.kt
fun main() = runBlocking {
    val flow = flowOf(1, 2, 3)

    // only collect first
    flow.first().println()
    // only collect last
    flow.last().println()
    // collect the count
    flow.count().println()
    // collect the reduced value
    flow.reduce { accumulator, value -> accumulator * value }.println() // 1 * 2 * 3
    // collect the reduced value with a initial value
    flow.fold(10) { accumulator, value -> accumulator + value }.println() // 10 + 1 + 2 + 3
}

// log
1
3
3
6
16


  上面的代码都比较简单,下面如果我们要自定义一个 average 操作符应该如何写呢?我们先来参考一下 count 操作符的实现:

// count
public suspend fun <T> Flow<T>.count(): Int  {
    var i = 0
    collect {
        ++i
    }
    return i
}


  又是如此简单清晰,直接在 collect 里面对一个全局变量做 ++ 操作,下面我们就以 Flow<Float>为扩展对象来自定义 average:

// FlowDeepDive17.kt
fun main() = runBlocking {
    flowOf(1f, 2f, 3f)
        .average()
        .println()
}

suspend fun Flow<Float>.average(): Float {
    var count  = 0
    var accumultor = 0f

    collect {
        count++
        accumultor += it
    }
    
    return accumultor / count
}

// log
2.0

  只需要再用一个 accumulator 来保存所有累加的值,最后用 accumultor / count 就得到了平均值。大家有没有注意到这些 collect 方法都是 suspend 的,而前面提到的操作符都不是 suspend 的,原因在于前面的操作符都是懒计算属性的,只有调用 collect 类方法的时候才真正开始收集数据,而因为 flow 是非阻塞式的,所以 collect 类方法需要是 suspend的,这也是识别普通操作符和 collect 类方法的一种方式。下面我们再介绍另外一种类型的操作符,在 rxjava 中,这些操作符主要体现在 collector 中,而 flow 则在 collector 中完全去掉了这些方法,精简了 collector。

Collector

fun main() = runBlocking {
    flowOf(1, 2, 3, 4)
        .onEach { println("onEach1 $it") }
        .onStart { println("onStart") }
        .onCompletion { println("onCompletion") }
        .map { if (it % 3 == 0) throw IllegalArgumentException() else it }
        .catch {
            println("catch $it")
            emit(-1)
        }
        .onEach {
            println("onEach2 $it")
            if (it == -1) throw RuntimeException()
        }
        .collect { println(it) }
}

// log
onStart
onEach1 1
onEach2 1
1
onEach1 2
onEach2 2
2
onEach1 3
onCompletion
catch java.lang.IllegalArgumentException
onEach2 -1
Exception in thread "main" java.lang.RuntimeException ...


  分析 log 我们知道 onStart 在一开始会被调用一次,onCompletion 在上游结束 emit 时会调用一次, onEach 会在其上游每次 emit 数据时发生时被调用,在异常发生后上游的会停止 emit 数据,此时异常会进入到 catch 中,如果我们在 catch 中再次 emit 一个数据,其下游还是会收到,我想大家对于 catch 最为好奇,我们就来看看 catch 是如何实现的:

public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> =
    flow {
        val exception = catchImpl(this)
        if (exception != null) action(exception)
    }

// catchImpl
internal suspend fun <T> Flow<T>.catchImpl(
    collector: FlowCollector<T>
): Throwable? {
  // 1. fromDownstream
  var fromDownstream: Throwable? = null
	try {
    		// e1: upstream
        collect {
          	// e2:downstream
            try {
                collector.emit(it)
            } catch (e: Throwable) {
                fromDownstream = e
                throw e
            }
        }
    } catch (e: Throwable) {
      // 3. return fromDownstream
  		return fromDownstream?:e
  }
  ...
}


  catch 操作符依然非常简单,创建了一个 flow,然后在 catchImpl 中返回一个 nullable 的 exception,如果 exception 非空,则执行我们传入的闭包。在上面的例子中我们在 catch 中 emit 了一个数据。

  说会到 catchImpl 也比较简单,但需要注意的,异常可能发生在两个位置,一个是上游 e1,这一般是我们期待的,还有就是可能发生在下游 e2,这里即往调用 emit 时可能发生的异常,这个下游仅限于 emit 方法本身,而不真正包含下游,所以上面的 catch 只能 catch 住上面发生的异常,当 onEach2 再次抛出异常时,整个异常就被抛出而不在 flow 链中了。由上面的代码可知,其实我们自己也可以用 try catch 包住 collect 过程来处理异常。

  这篇文章已经够长了,我们就在这里先停住了。

总结

Flow 可以被分为 Builder,Operator,Collector 三个部分

Builder:我们可以把任何数据转化为 Flow,我们介绍了一些官方的 build flow 的方法和转化为 Flow 的扩展

Operator: Operator 大多也是通过 build 一个新的 flow,在两次 flow 之间做变换来实现的,主要包括,map,filter,flat,collect 类的 operator,collect 类的都是 suspend 的方法,而其他的则是懒计算的,所以无需 suspend

Collector:类似于 Rxjava 的Collector 在 flow 中被精简为只有一个 collect 方法,而诸如 onStart,onCompletion,catch 之类的类生命周期方法则被提到了 operator 之中

   本篇我们通过把 Flow 拆分为三个部分,并深入研究了一种一些操作符的实现。接下来要讲什么,读者可以在评论区讨论互动,笔者会根据互动的情况来安排后面的内容。

示例源码github.com/chdhy/kotli…

练习:查看 debounce 操作符的源码,体会把 callback 变为 flow 的好处

点赞👍文章,关注❤️ 笔者,获取其他文章更新

  1. 「最后一次,彻底搞懂kotlin协程」(一) | 先回到线程

  2. 「最后一次,彻底搞懂kotlin协程」(二) | 线程池,Handler,Coroutine

  3. 「最后一次,彻底搞懂kotlin协程」(三) | CoroutineScope,CoroutineContext,Job: 结构化并发

  4. 「最后一次,彻底搞懂kotlin协程」(四) | suspend挂起,EventLoop恢复:异步变同步的秘密

  5. 「最后一次,彻底搞懂kotlin协程」(五) | Dispatcher 与 Kotlin 协程中的线程池

  6. 「最后一次,彻底搞懂kotlin协程」(六) | 全网唯一,手撸协程!

  7. 「最后一次,彻底搞懂kotlin Flow」(一) | 五项全能 🤺 🏊 🔫 🏃🏇:Flow

  8. 「最后一次,彻底搞懂kotlin Flow」(二) | 深入理解 Flow

  9. 「最后一次,彻底搞懂kotlin Flow」(三) | 冷暖自知:Flow 与 SharedFlow 的冷和热