Kotlin: Flow基础知识

802 阅读5分钟

Kotlin Flow/Sequence/Channel 实战原理与场景详解

1. 基本概念对比

  • Flow:数据流(连续),同协程下的数据序列持续发送与处理。
  • SharedFlow:事件流(独立),支持事件序列向多个订阅者(跨协程)一对多通知。
  • StateFlow:状态流(独立),是特殊SharedFlow,能订阅当前状态,本质上也是事件。

2. Sequence的机制和与List的区别

Flow,可以称之为协程版本的sequence,类似于队列的机制,队头插入数据,队尾取出数据,其实只要有这样的机制的数据结构我们都可以称之为队列,例如LinkedList。Sequence就是按照队列的方式来提供数据,并且按照提供的顺序来获取数据的Api,但和队列的区别在于,它不是数据结构,而是一种机制。因为它让我提供的是数据的规则,而不是数据的本身,我们用Sequence来提供的数据,实际上是动态生产的,用完一条才生产下一条,而不是像队列那样,提前把数据都准备好了,然后一个一个取用。当然队列也可以实现容量是1,就是生产一条取一条。不生产就没有。Sequence是从机制上就限制了,生产一条使用一条,使用完成之后再去生产一条新的数据的机制。

val nums = sequence {
  while (true) {
    yield(1)
  }
}

协程里边有一个yield函数,它是手动的让协程暂停一下,和线程的Thread.yield()是一样的定位。通过强行让自己暂停的方式让出当前暂用的线程。来手动调节线程占用的,本质上就是在当前时间片还没有到时间的时候,就主动放弃时间片。属于很底层的函数。Sequence里边也有yield的函数,它是用来生产数据的,和协程的yield完全无关。

小结:

  • Sequence没有内部数据,只是“生成规则”,只有遍历时才会产生数据。
  • List是全部数据预先存好,遍历只是读取。
  • Sequence更适合“惰性生成”/数据量巨大/无限流的场景。

3. Sequence和List的懒加载举例

val list = buildList {
  while (true) { // list 永远无法执行完,直到内存溢出。
    add(getData())
  }
}
for (num in list) {
  println("List item: $num")
}

val nums = sequence {// sequence的机制保证了它的数据是使用者需要数据的时候才会生产。
  while (true) {
    yield(1)
  }
}
for (num in nums) {
  println("List item: $num")
}

总结补充:

  • 用list无限add会OOM;sequence不会,永远只生产用到的那一条数据。
  • Sequence没有内部数据,是生产规则。
  • Sequence能更快开始处理数据,但如果你全部用完,和list整体性能差不多。

4. Sequence只能用自己的挂起函数,不能用suspend

在sequence里边,是不能调用不属于Sequence类的挂起函数,调用其余的挂起函数都会报错。为什么会有这个限制呢?这是因为kotlin团队希望Sequence使用协程来实现业务逻辑,但这套协程的逻辑是完全独立的、与世隔绝的,避免其他环境的协程代码进入执行时,影响Sequence的初始逻辑。从本质上讲,这种只能用自己挂起函数的限制,是提供了一种场景化的特权。

举例:

val nums = sequence {
  // delay() 调用就会报错。
  while (true) {
    yield(1)
    yieldAll()
  }
}

结论
Sequence更适合纯同步流式数据。如果要异步挂起、比如网络请求,就要用Flow。


5. Flow的本质和冷/热流原理

Flow的工作模式:设定好一套逻辑,在每个collect地方都重复执行一遍这个逻辑,在这套逻辑里边安插的一些发送数据的节点,而collect执行这一套逻辑的时候,对于每一条数据都会执行自己设定好的数据处理逻辑,这就是Flow的本质。由Flow对象来提供生产数据流的生产逻辑,然后在收集流程里执行这套生产逻辑,并处理每条数据,所以说Flow就是一个数据流工具。

冷流/热流简述

  • Channel是热流,数据可以在没有collect时就被生产。
  • Flow是冷流,只有collect时才生产。

例子:

val numsFlow = flow {
  emit(1)
  delay(100)
  emit(2)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
  numsFlow.collect {
    println("A: $it")
  }
}
scope.launch {
  delay(50)
  numsFlow.collect {
    println("B: $it")
  }
}

6. Flow和Sequence收集方式区别

val nums = sequence {
  while (true) { yield(1) }
}
for (num in nums) {
  println("List item: $num")
}

// Flow中的Collect方法或报错,因为collect方式是suspend修饰的,必须在协程中执行。
val numsFlow = flow {
  while (true) { emit(getData()) }
}.map { "number $it" }
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
  numsFlow.collect { println(it) }
}
  • Sequence可以普通for-in直接用。
  • Flow的collect是挂起的,必须在协程作用域中调用

7. Flow的适用场景

如果想把数据的提供和数据的处理两个流程拆分开来,就需要使用Flow了。这种持续提供数据的形式,就是数据流,而需要把数据流的生产和消费功能拆分开的业务场景就是Flow的用途所在。

典型代码:

val weatherFlow = flow {
  while (true) {
    emit(getWeather())
    delay(60000)
  }
}

suspend fun showWeather(flow: Flow<String>) {
  flow.collect {
    println("Weather: $it")
  }
}
  • 持续数据/异步/网络/耗时,适合用Flow。
  • 普通同步、只需要一次性数据处理,Sequence/List更简单。

8. Flow的创建方式全汇总

flow { emit(1) }                    // 1. 最基础
flowOf(1,2,3)                       // 2. 直接用
listOf(1,2,3).asFlow()              // 3. List转Flow
sequenceOf(1,2,3).asFlow()          // 4. Sequence转Flow

// 5. 用Channel创建的Flow(注意冷热流区别)
val channel = Channel<Int>()
val flow5 = channel.consumeAsFlow()
val flow6 = channel.receiveAsFlow()
  • channel.consumeAsFlow只能消费一次;receiveAsFlow每次collect都新建一个receive。

9. channelFlow 和 callbackFlow 的意义

创建Flow还有两种方式,channelFlow和callbackFlow:
这两种方式与上边的 consumeAsFlow 和receiveAsFlow的区别是,上边两种方式共用了一个Channel,所有数据的消费都需要通过collect进行,会瓜分channel生产的数据。而后边这两种方式,它创建的Flow是直到Collect的时候才会创建Channel,开始生产。也就是,对此调用就会创建多个Channel,这些Channel的生产流程是互相隔离、各自独立的。如果还用冷热概念来说的话,consumeAsFlow 和receiveAsFlow是热的Flow,而 channelFlow 与callbackFlow 是冷的Flow。

channelFlow的作用

  • 允许在block里启动子协程、可以跨协程emit数据(Flow不能这样)。

callbackFlow专为回调API适配

  • 必须调用awaitClose,否则会报错。
val flow7 = channelFlow {
  launch {
    delay(2000)
    send(2)
  }
  delay(1000)
  send(1)
}

val flow9 = callbackFlow {
  gitHub.contributorsCall("square", "retrofit")
    .enqueue(object : Callback<List<Contributor>> {
      override fun onResponse(call: Call<List<Contributor>>, response: Response<List<Contributor>>) {
        trySend(response.body()!!)
        close()
      }
      override fun onFailure(call: Call<List<Contributor>>, error: Throwable) {
        cancel(CancellationException(error))
      }
    })
  awaitClose()
}

10. 为什么Flow不能跨协程emit?

这是因为我们的collect在处理数据的时候,是在发送数据的各个协程里边处理的,这也就导致了,各个协程可能所在的Dispatcher是不一样的,最终会导致软件的运行所在线程可能与开发者的预期不一致。处理这个问题有两种方式,一种是上层业务方自己使用的时候注意切换写成,另一种就是底层,从api的设计就限制了flow,禁止在别的协程调用flow来发送数据,也就是emit必须在flow自己的代码块中发送,强制保证数据一定是从collect所在的协程发送的。

核心总结:

  • Flow限制emit只能在block当前协程,是为了保证数据顺序和一致性,防止多线程乱序难以排查。
  • 如果有多线程/跨协程emit的需求,直接用channelFlow!

11. SharedFlow和StateFlow的本质

SharedFlow 是冷流和热流的中间态,适合事件总线、多个订阅者共享事件流。
StateFlow 是带有“最新状态”的 SharedFlow,订阅者随时能拿到当前值。用在状态驱动(如UI),而不是简单事件。


12. 总结和使用建议

  • 有异步、挂起、数据量大、事件流用Flow系列。
  • Sequence/List适用于本地、小量、同步数据流。
  • 多线程/回调/异步事件流用channelFlow或callbackFlow。
  • 只要能用更简单的方式,不要滥用Flow。

学后检测

1. 单选题:下列关于 Sequence 和 List 的区别描述正确的是?

A. Sequence 在声明时会立即生成所有数据
B. List 在遍历时才生产数据
C. Sequence 只有在被消费时才生产数据
D. List 只能存储基本类型数据

答案:C
解析: Sequence是惰性计算,只有被消费(遍历)到时才生产数据;List在构造时所有数据就准备好了。


2. 单选题:下列关于 Kotlin Flow 特性的说法,错误的是?

A. Flow 支持使用挂起函数生产数据
B. Flow 的 collect 必须在协程作用域内调用
C. Flow 一定是热流
D. Flow 可以用来实现持续的异步数据流

答案:C
解析: Flow 默认是冷流,只有 collect 时才会开始生产数据。


3. 判断题:Flow 的 collect 是一个挂起函数,可以直接在主线程上调用。

答案:错
解析: collect 是挂起函数,必须在协程环境下调用,不能在普通主线程上直接用。


4. 单选题:下列哪种方式可以实现跨协程 emit 数据到 Flow?

A. 普通 flow{}
B. channelFlow{}
C. flowOf()
D. listOf().asFlow()

答案:B
解析: 只有 channelFlow{} 支持在内部 block 启动子协程跨协程 emit;普通 flow{} 不能。


5. 多选题:关于 channel.consumeAsFlow() 和 channelFlow{},下列描述正确的是?

A. consumeAsFlow() 返回的 Flow 只能被 collect 一次
B. channelFlow{} 每次 collect 都会新建 Channel
C. consumeAsFlow() 是热流,channelFlow{} 是冷流
D. channelFlow{} 适合回调API转数据流

答案:A B C D(全选)
解析: consumeAsFlow只能collect一次,多次报错;channelFlow每次collect新建channel,是冷流;consumeAsFlow是热流;channelFlow/callbackFlow用于回调API和多协程生产。


6. 简答题:为什么 Kotlin 的 Sequence 不能在 block 内使用 suspend 函数?如果你想在生产数据时用 delay() 应该用什么?

答案参考:
Sequence 的协程上下文是“独立隔离”的,为了防止其它挂起函数改变流程或线程导致混乱,因此被 @RestrictsSuspension 注解限制,只能用 yield 系列。
如果要在生产数据时用 delay() 等挂起函数,应该用 Flow 或 flow{} 构建数据流。


7. 简答题:举例说明 StateFlow 适合解决什么类型的业务场景?请给出使用代码片段。

答案参考:
StateFlow 适合用来做 UI 层的状态同步,比如页面主题切换、登录状态、网络状态等,只要 StateFlow 的值被更新,所有订阅者都会收到最新值。

代码例子:

val stateFlow = MutableStateFlow("未登录")

fun login() {
    stateFlow.value = "已登录"
}

scope.launch {
    stateFlow.collect { status ->
        println("当前登录状态:$status")
    }
}

8. 简答题:你用 Flow 实现了一个消息流,结果发现有时候消息被丢了没收集到,可能是什么原因?怎么改进?

答案参考:
原因1:Flow是冷流,每次collect才生产,collect时机不对会丢消息;
原因2:如果用 Channel 转 Flow,多个 collect 会瓜分 channel 数据,导致有些collect拿不到数据。
改进:如果要实现消息广播给多个订阅者,应该用 SharedFlow;如果只是点对点消费,注意不要多个协程同时 collect 同一个 channelFlow/consumeAsFlow。


9. 编程题:用 callbackFlow 封装一个 Android 原生的网络回调API,将回调转换为 Flow,要求所有 collect 都能独立收到数据。

答案参考:

fun retrofitAsFlow(): Flow<String> = callbackFlow {
    val call = api.getData()
    call.enqueue(object : Callback<String> {
        override fun onResponse(call: Call<String>, response: Response<String>) {
            trySend(response.body() ?: "")
            close()
        }
        override fun onFailure(call: Call<String>, t: Throwable) {
            cancel("请求失败", t)
        }
    })
    awaitClose { call.cancel() }
}

解析: callbackFlow 每次 collect 都新建一次,互不影响,适合回调转Flow场景。