学习 flow 之前,最好先学习一下 kotlin 的 协程 和 Channel,flow 是属于协程标准库的。但是 flow 的思想和 RxJava 一致,如果熟悉 RxJava,学起来也没有门槛。你可以认为 flow 就是 kotlin 版的 RxJava。Channel 是用于协程之间通信的的数据通道。flow 的部分操作符的原理借助了 Channel 来实现。
kotin 官方文档有提到 flow 的灵感来源于 RxJava,所以看起来这两者很像,不同的是 flow 的是基于 kotlin,基于协程。
对于使用者才说,flow 的 api 很友好,相比 RxJava 很直观简洁容易理解。
flow 文档: coroutines-Asynchronous Flow
为什么要学习 flow?
优点:
- 目前 kotlin 是官方亲儿子,无缝衔接 kotlin
- 只需引入 kotlin 协程库就送一个 flow,方法数和体积都小,本身就是基于协程实现,所以和协程完美融合
- 简洁友好的 api,这一点给我的感觉就是 api 字面上的意思对应的就是实际的操作,不看源码只看 api 就知道是什么意思。
- 自定义操作符相对容易(看了一些自带操作符的实现,没有 rxjava 那么复杂)
缺点:
- 操作符没有 RxJava 那么丰富,但是够用,部分操作符还处于预览状态
- 你的 kotlin 版本最好 >= 1.3.0,因为 1.3.0 是 flow 的第一个 stable release
基本概念和使用
和 RxJava 类似,flow 也是基于流式(Stream)的数据处理。也是观察者模式,为了方便理解,以下的例子都会和 RxJava 作比较来方便理解。
先看一个 RxJava 的例子:
Observable.create<Int> {e->
listOf<Int>(1,2,3).forEach {
e.onNext(it)
}
}.subscribe {
println(it)
}
// 日志:
// 1
// 2
// 3
这段代码将 list 的数据依次发射了出去然后打印出来。
Observable 默认是 cold 的,必须订阅才能发射数据。create 创建数据源,通过 subscribe 订阅激活数据源的发射。
我们来看看相同逻辑的 flow 的实现:
flow {
listOf<Int>(1,2,3).forEach {
emit(it)
}
}.collect{
println(it)
}
// 日志:
// 1
// 2
// 3
很简单,照猫画虎就可以直接上手了。flow 函数负责创建数据源,对应 RxJava 的 create,collect 对应 RxJava 的 subscribe。同样 flow 默认也是 cold 的,只有在 collect 之后数据流才会发射。
flow 使用 emit 来发射数据
对应 RxJava 的 onNext。
flow 中也有一个创建数据流便捷的方法:flowOf()
-
flowOf(1):等价于 Observable.just(1)
-
flowOf(1, 2, 3):等价于 Observable.fromArray(1, 2, 3),按顺序依次发射 1,2,3
切协程
相信大家初次使用 RxJava 基本都是用来做网络请求切线程(杀鸡用牛刀^_^):
fun getHttp(){
httpObservable.subscribeOn(IoThread)
.observeOn(MainThread)
.subscribe{
// success
}
}
那么 flow 怎么切线程呢?相比较 RxJava,flow 设计的很简单, 只提供了一个 flowOn 函数来切协程。在 flowOn 上游的作用域会在其协程中执行,看如下代码:
suspend fun getFlow(){
flow {
// 运行在 dispatcher1
listOf(1, 2, 3).forEach {
emit(it)
}
}.flowOn(dispatcher1).map {
// 运行在 dispatcher2
"2"
}.flowOn(dispatcher2).collect {
// 运行的协程取决于整个 flow 在哪个协程调用
println(it)
}
}
可以看到除了 collect 每个 flowOn() 都会影响它之前上游作用域运行的协程。那么 collect 运行在哪个协程呢,取决于你在哪个协程里调用 getFlow。
launch(dispather3){
// collect 运行在 dispather3
getFlow()
}
好了,学会怎么切协程之后,你就可以把你的 RxJava 网络请求那一套换掉了(手动狗头):
fun getHttp(){
launch(mainDisPather){
flow<HttpModel>{
// 发起请求
}
.flowOn(ioDispather)
.collect{
// success
}
}
}
捕获异常
上述的网络请求在发生 404 之类的错误时,retrofit 框架是直接会抛出一个 HttpException 的,我们的程序就会直接 crash 了。而 RxJava 却没啥事。因为在 RxJava 中,会自动帮我们捕获大部分的 error,捕获不了的,你可以在 RxJavaPlugin 去自定义捕获。那么 flow 怎么捕获呢?
flow 提供了一个操作符: catch()
flow<Int> {
emit(1)
emit(2)
emit(3)
emit(4)
}.map{
if(it > 2){
throw NullPointerException("不应该为 2")
}else{
it
}
}.catch { e ->
println(e)
}.collect {
println(it)
}
// log:
// 1
// 2
// java.lang.NullPointerException:
catch 操作符会把上游的错误捕获,而上游一旦发生错误,数据流就不再向下游发射数据了。所以上述代码只会收到 1 和 2。
这样的设计我觉得更加灵活,按需去捕获异常,RxJava 一股脑的把错误捕获了,有的时候异常直接被 RxJava 吞掉了,你的程序出现了不应该出现的异常而你却不知道,有时候这是很难去发现的。
如果在捕获错误的同时再发射一个值,就等价于 RxJava 的 onErrorReturn()
flow.catch{
emit(0)
}
回调
在 RxJava 中我们经常使用一些的一些回调,flow 有没有呢?有的!
-
onStart:数据流开始发射
-
onEach:数据流中的每一个数据发射时的回调
-
onCompletion:数据流结束发射
-
onEmpty:当数据流中没有发射任何数据时
-
catch: 发生异常时
用这些回调可以结合业务做一些提示,比如 onStart 的时候就会显示一个 loading,onEmpty 说明请求的数据为空,显示当前没有任何数据。catch 说明请求中出错了,提示错误的信息。onCompletion 代表请求结束,将 loading 隐藏。
flow {
emit(1)
emit(2)
emit(3)
emit(4)
}.catch { e ->
// 发生了异常。显示异常信息
showToast("加载错误")
}.onEmpty {
// 空白数据
showToast("什么数据都没有")
}.onStart {
showToast("正在加载中")
}.onEach {
showToast("开始处理 $it")
}.onCompletion { e->
if(e == null){
showToast("加载结束 $it")
}else{
showToast("加载失败 $e")
}
}.collect {
showToast("加载成功 $it")
println(it)
}
背压
背压的定义:上游发射数据的速度太快了,下游来不及处理。
上游每 1s 发射一个数据,下游每 2s 才处理完一个数据。
和线程池的饱和拒绝策略也是一个道理:RejectedExecutionHandler
生产者的产出速度过快,消费者来不及处理。
flow 不可避免的也绕不过背压的限制,下游无法控制上游发数据的速度,下游需要作出应对策略。
处理背压无非就是缓存之后怎么去取。
先来回忆下 RxJava 的背压策略:RxJava之背压策略
- MISSING:缓存满了就抛出异常MissingBackpressureException
- ERROR:直接抛出异常MissingBackpressureException
- BUFFER:无限增大缓存,不丢任何数据,
- DROP:缓存满了之后,新的数据会被丢弃
- LATEST:缓存满了之后,始终会把最新的数据加到缓存区的最后一个
flow 的策略特别简单,使用一个操作符解决:buffer(capity, onBufferOver):
-
capacity:缓存的大小
-
onBufferOverflow:缓存超出的策略。
-
SUSPEND:挂起,等待执行,不丢弃任何数据。效果等同 RxJava 的 BUFFER,但是由于协程的加持不会占用缓存
-
DROP_OLDEST:丢掉老的数据,只处理新数据。等同 RxJava 的 LATEST
-
DROP_LATEST:丢掉新的数据。等同 RxJava 的 DROP
buffered values /-----------------------\ queued emitters /----------------------\ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | | 1 | 2 | 3 | 4 | 5 | 6 | E | E | E | E | E | E | | | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
如果 buffer size = 4
DROP_OLDEST: [ 1,2,3,4] <- [5,6,7] , [1,2,3] 会被丢弃变成 -> [4,5,6,7] DROP_LATEST: [ 1,2,3,4]<- [5,6,7] [5.6.7] 会被丢弃变成-> [1,2,3,4]
基于 buffer 衍生了一系列和背压有关的操作符:
-
conflate(): 只取最新的数据,等价 buffer(0, DROP_OLDEST),即不缓存数据,直接取最新数据的处理
-
collectLatest():类似conflate,但是不会直接用新数据覆盖老数据,而是每一个都会被处理,只不过如果前一个还没被处理完后一个就来了的话,处理前一个数据的逻辑就会被取消。
-
mapLatest:同理 collectLatest
-
flatMapLatest:同理 collectLatest
对比 flow 和 Rxjava 可以发现策略是完全一样的。都是基于缓存做了一些策略处理。只是 flow 没有错误抛出而已。并且默认的挂起 SUSPEND 不会有内存溢出问题。
// buffer 源码
public fun <T> Flow<T>.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND): Flow<T> {
// 缓存容量必须是 >=0 / BUFFERED/CONFLATED
require(capacity >= 0 || capacity == BUFFERED || capacity == CONFLATED) {
"Buffer size should be non-negative, BUFFERED, or CONFLATED, but was $capacity"
}
// CONFLATED 和 SUSPEND 挂起冲突
require(capacity != CONFLATED || onBufferOverflow == BufferOverflow.SUSPEND) {
"CONFLATED capacity cannot be used with non-default onBufferOverflow"
}
// desugar CONFLATED capacity to (0, DROP_OLDEST)
var capacity = capacity
var onBufferOverflow = onBufferOverflow
if (capacity == CONFLATED) {
capacity = 0
onBufferOverflow = BufferOverflow.DROP_OLDEST
}
// create a flow
return when (this) {
is FusibleFlow -> fuse(capacity = capacity, onBufferOverflow = onBufferOverflow)
else -> ChannelFlowOperatorImpl(this, capacity = capacity, onBufferOverflow = onBufferOverflow)
}
}
// 丢弃缓存里老的值,缓存新的值
flow {
repeat(10000) {
emit(it)
}
}.buffer(capacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST).collect {
println("backpressureDemo :$it")
}
// log
// backpressureDemo :0
// backpressureDemo :9990
// backpressureDemo :9991
// backpressureDemo :9992
// backpressureDemo :9993
// backpressureDemo :9994
// backpressureDemo :9995
// backpressureDemo :9996
// backpressureDemo :9997
// backpressureDemo :9998
// backpressureDemo :9999
// 丢弃新的值
flow {
repeat(10000) {
emit(it)
}
}.buffer(capacity = 10, onBufferOverflow = BufferOverflow.DROP_LATEST).collect {
println("backpressureDemo :$it")
}
// log
// backpressureDemo :0
// backpressureDemo :1
// backpressureDemo :2
// backpressureDemo :3
// backpressureDemo :4
// backpressureDemo :5
// backpressureDemo :6
// backpressureDemo :7
// backpressureDemo :8
// backpressureDemo :9
// backpressureDemo :10
所以从背压来看 flow api 设计的是不是超级简洁,一目了然?
得益于协程的挂起操作,默认我们不做任何背压处理也不会出现问题,会按顺序慢慢执行。对比 RxJava 那边是不是很方便,RxJava Flowable 写起来也怪麻烦的,强制指定背压策略,- 换 flow 吧!
操作符
除了 flowOn,几乎 RxJava 中的操作符在 flow 里都有对应的。flow 的操作符分类这几大类,每一大类在源码中都有一个独立的 kt 文件
操作符很多,就不一一赘述了,先说几个我们 RxJava 里最常用的。
map
对应 RxJava 的 map
这个很简单,变换操作符。 将数据 A -> B
flowOf(A).map{
B
}
flatMapConcat(目前还是预览版,后续版本可能会发生改动)
对应 RxJava 的 concatMap
将 flow 里面的值铺平分别再迭代发射,发射是有序的,和生产数据的顺序一致
// 将 [1,2,3,4,5] 依次发射,再根据 每个值继续发射 一个flow
flow {
List(5) { emit(it) }
}.flatMapConcat {
flow { List(it) { emit(it) } }
}.collect {
println(it)
}
// 输出:
// 0,0,1,0,1,2,0,1,2,3,0,1,2,3,4
可以看到上面的输出是完全有序的。
flatMapMerge(目前还是预览版,后续版本可能会发生改动)
对应 RxJava 的 flatmap
和 flatMapConcat 类似,但是呢,它的发射是并发的。
// 在 io 协程中才有可能并发
launch(io){
flow {
List(5) { emit(it) }
}.flatMapMerge {
flow { List(it) { emit(it) } }
}.collect {
println(it)
}
}
// 输出:
// 0,0,0,1,0,1,1,2,2,3
结果是乱序的,必须在 io 环境下协程才是乱序的哦,如果你启动的还是一个单线程协程,那还是顺序的
flatMapConcat 和 flatMapMerge 内部的实现其实差不多。
@FlowPreview
public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R> = map(transform).flattenConcat()
@FlowPreview
public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T> = flow {
collect { value -> emitAll(value) }
}
@FlowPreview
public fun <T, R> Flow<T>.flatMapMerge(concurrency: Int = DEFAULT_CONCURRENCY, transform: suspend (value: T) -> Flow<R>): Flow<R> = map(transform).flattenMerge(concurrency)
// 如果并发数量 concurrency == 1那么转为顺序发射,否则是利用 ChannelFlowMerge 来进行并发发射的
@FlowPreview
public fun <T> Flow<Flow<T>>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow<T> {
require(concurrency > 0) { "Expected positive concurrency level, but had $concurrency" }
return if (concurrency == 1) flattenConcat() else ChannelFlowMerge(this, concurrency)
}
zip
等价 RxJava 的 Observavle.zip()
flow {
delay(2000)
emit(2)
}.zip(flow {
delay(1000)
emit("2")
}){a,b->
println(a)
println(b)
a to b
}.collect {
println(it)
}
等待 2 个 flow 都发射完成,然后合并为一个 flow 统一发射
buffer
对应 RxJava Observable.buffer(),但是 RxJava 会将缓存的数据合并成一个 List 再发射。flow 还是一个一个的发出去。
缓存数据,见上文 #背压 的介绍。flow 将背压的处理整合到了 buffer 操作符
debounce
防抖,等价 Observable.debounce,在 L 时间内,取最后一次发射的值。
flow{
repeat(5){
emit(it)
}
}.debounce(200).collect {
println(it)
}
// 输出 4
sample(目前还是预览版,后续版本可能会发生改动)
等价 RxJava 的 sample
阶段性的取一定时间内的最后一个数据
val flow = flow {
emit("A")
delay(1500)
emit("B")
delay(500)
emit("C")
delay(250)
emit("D")
delay(2000)
emit("E")
}
// 取每 1000ms 时间内发送的最后一个数据
val result = flow.sample(1000).toList()
assertEquals(listOf("A", "C", "D"), result)
上述的例子每隔一段时间会发送数据 A、B、C、D 等,我们的把这些时间以 1000 ms 为单位进行分割:
1000 2000 3000 4000 5000
|-----|-----|-----|------|-----|
A B C D E
可以直观的看到每个1000 ms 的最后一个发射的数据。D 和 E 之间的间隔超过了 1000 ms ,数据走到这里就会发射中断了,不管 E 之后的数据是否发射间隔存在 1000m 内的。
在第 2 个 1000ms 时,C 刚好处于分界点上,官方没有对这一特殊情况进行说明,从实际运行结果看,此时 C 被认为是第二个 1000ms 发射的最后一个数据。
这里官方的 test case 写的 期望结果是 [A,B,D],而实际的运行结果是 [A,C,D]。
感觉是官方的 bug。这个 sample 目前还是预览版,不建议大家使用。
热的 flow
冷和热的定义:
-
冷:观察者订阅数据源后才会发射数据
-
热:数据源自己可以随意发射数据
RxJava 和 flow 一样默认也是冷的。
RxJava 中 的 PublishSubject 就是热的数据源:RxBus 就是用它来实现的。协程之间使用 channel 来传输数据,那么 channel 天生就是可以作为热的数据通道,只管发送数据。
RecicedChannel 和 BroadcastChannel(预览版)
关于 Channel 的基础知识:破解 Kotlin 协程(9) - Channel 篇
- RecicedChannel:一对一发送,同一个数据只能被一个接受者接收
- BroadcastChannel:广播发送,一对多发送,同一个数据能被所有接受者接收
借助 receiveAsFlow 或者 consumeAsFlow 可以把 Channel 转化成 hot flow。
-
receiveAsFlow:可以反复消费 flow (同一个 receiveAsFlow 你可以多次调用 collect 进行消费数据)
-
consumeAsFlow:flow 只能被消费一次(同一个 consumeAsFlow 你只能调用1次 collect 进行消费数据)
fun receiveChannel() { val receiveContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher() val producer = GlobalScope.produce { var count = 0 while (true) { send("hello-count") count++ delay(1000) } } val consumeAsFlow = producer.receiveAsFlow() GlobalScope.launch(receiveContext) { consumeAsFlow.collect { println("Ait") } }
GlobalScope.launch { consumeAsFlow.collect { println("B$it") } } }
// 输出:
Ahello-0 Ahello-1 Bhello-2 Ahello-3 Bhello-4 Ahello-5 Bhello-6 Ahello-7 Bhello-8 Ahello-9 Bhello-10 Ahello-11 Bhello-12 Ahello-13 Bhello-14 Ahello-15
可以看到 RecicedChannel 的结果是互斥的,同一个元素只能被一个消费者去消费。
RecicedChannel 适合一对一消费的场景的,一个数据只会被消费一次。
借助于 asFlow 扩展函数,我们可以把一个 BroadcastChannel 转化成 hot flow
fun broadcastChannel() {
val receiveContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val producer = GlobalScope.broadcast<String> {
var count = 0
while (true) {
send("hello-$count")
count++
delay(1000)
}
}
val consumeAsFlow = producer.asFlow()
GlobalScope.launch(receiveContext) {
consumeAsFlow.collect {
println("A$it")
}
}
GlobalScope.launch {
consumeAsFlow.collect {
println("B$it")
}
}
}
// 输出
Ahello-0
Ahello-1
Bhello-1
Ahello-2
Bhello-2
Ahello-3
Bhello-3
Ahello-4
Bhello-4
Ahello-5
Bhello-5
Ahello-6
Bhello-6
Ahello-7
Bhello-7
Ahello-8
Bhello-8
可以看到 broadcast 的输出是一对多,同一个元素被多个消费者消费了。
BroadcastChannel
适合一对多消费的场景的,一个数据只会被消费n次,n = 消费者的个数。
BroadcastChannel
相关的 API 大部分被标记为ExperimentalCoroutinesApi
,后续也许还会有调整。
ShareFlow
Flow 里有一种 叫做 ShareFlow 实现了这种热发送数据。
ShareFlow 的功能和 BroadcastChannel 几乎一样,但是 ShareFlow 的实现没有使用 Channel Api。并且是直接提供了 flow 的返回,并且多了下面几个特性:
-
更简洁的 api,因为内部没有使用 Channel Api
-
支持配置 replay 和 buffer
-
提供一个只读的 ShareFlow 和 可写 MutableShareFlow
-
SharedFlow 不可用被关闭,而 channel 是可以主动关闭的
ShareFlow 用法和 Channel 差不多**,**我们来写一个简单的 flow 版的 eventbus 来看看 MutableSharedFlow 的使用场景:
object FlowEventBus {
// 使用 MutableSharedFlow 来作为事件的通道
private val _events = MutableSharedFlow<Event>() // private mutable shared flow
val events = _events.asSharedFlow() // publicly exposed as read-only shared flow
suspend fun produceEvent(event: Event) {
_events.emit(event) // suspends until all subscribers receive it
}
}
// 接收事件
GlobalScope.launch(sendContext) {
FlowEventBus.events.collect{
println(it.name)
}
}
// 发送事件
GlobalScope.launch(recieveContext) {
repeat(20){
delay(2000)
FlowEventBus.produceEvent(Event("a${it}"))
}
}
MutableSharedFlow 有 3 个 参数
-
replay:缓存之前发送的过的数据的数量,如有新的订阅者就把值再次发送。和 RxJava 的 BehaviorSubject 是相同的效果,可以用来实现粘性事件的效果
-
extraBufferCapacity:缓存的数量
-
onBufferOverflow:缓存满了之后的策略
extraBufferCapacity 和 onBufferOverflow 和上文的 #buffer 部分是一样的。replay 则和 RxJava 的 BehaviorSubject 效果一样。
所以我们实现 hot flow 最佳方案应该选用 ShareFlow 。
这里是我学习 shareFlow 实现的 flowbus:github.com/lwj1994/flo…
总结
flow 作为协程的一部分,无缝配合协程且继承了 Reactive 响应式架构的威力。简洁明了的 api,完全可以替代 RxJava 。