Kotlin flow 的创建原理和流程
可以认为:Flow是协程和响应式编程的结合体。
引言
最近项目中涉及到了一些硬件设备搜索的需求,逻辑比较繁杂。主要内容是通过UDP/TCP识别设备后再通过HTTP获取相应数据。于是就采用了Flow的方式去实现。但有小伙伴却一不小心对Flow多次执行了collect
。由此引发了一些意想不到的问题。
在了解Flow之前,最好先了解一下Sequence序列。其简单用法如下:
fun sequenceDemo(){
val numbers = sequence {
repeat(3){
yield(it)
}
}
println(numbers.toList())
}
Flow的用法类似:
fun flowDemo(){
val numbers = flow {
repeat(3){
emit(it)
}
}.flowOn(Dispatchers.IO)
runBlocking {
numbers.collect{
println(it)
}
}
}
用法相似,但是却又有两点明显的区别:
- sequence由于受到RestrictsSuspension的约束,SequenceScope的扩展里(也就是sequence{}代码块)是无法使用挂起函数的。Kotlin特意增加了这个限制,用来确保sequence在整个执行过程中不会发生线程切换。这带来序列不支持协程上下文,也无法通过调度器制定序列创建的线程。这大概率就是Flow诞生的原因吧!
- Flow支持了线程调度,可以使用flowOn设定它运行时所在的线程。同时,它支持挂起函数和协程上下文。最终通过collect消费flow的数据,collect是一个挂起函数。这意味着它必须在协程或其他挂起函数内被调用。
对Flow多次执行collect会发生什么
现在回到正题,如果对对Flow多次执行collect会发生什么,也就是多次消费flow会怎样?直接通过下面代码查看输出结果:
fun flowDemo(){
val numbers = flow {
repeat(10){
delay(10)
emit(it)
}
}.flowOn(Dispatchers.IO)
GlobalScope.launch(Dispatchers.IO) {
numbers.collect{
println("第一个:${it}")
}
}
GlobalScope.launch(Dispatchers.IO) {
numbers.collect{
println("第二个:${it}")
}
}
}
输出结果如下:
第二个:0
第一个:0
第二个:1
第一个:1
第二个:2
第一个:2
....省略
第二个:7
第一个:7
第一个:8
第二个:8
第一个:9
第二个:9
可以看到,两次collect没有任何关联。flow被消费了两次。可以发现多次消费则多次生产,生产和消费总是相对应的。两次消费没有认识关联。但是如果讲代码修改为下面的样子:
fun flowDemo(){
val numbers = flow {
repeat(10){
emit(getNumber())
}
}.flowOn(Dispatchers.IO)
GlobalScope.launch(Dispatchers.IO) {
numbers.collect{
println("第一个:${it}")
}
}
GlobalScope.launch(Dispatchers.IO) {
numbers.collect{
println("第二个:${it}")
}
}
Thread.sleep(10000)
}
var i = 1
suspend fun getNumber():Int {
delay(10)
return i++
}
输出结果如下:
第二个:2
第一个:1
第二个:4
第一个:3
....省略
第一个:6
第二个:9
第一个:9
第一个:10
第二个:10
....省略
第一个:17
第二个:18
很明显,发生了线程安全问题。要想解决这个问题也很简单,只需要为getNumber加锁,修改后的代码如下:
var i = 1
val mutex = Mutex()
suspend fun getNumber(): Int {
mutex.withLock {
delay(10)
return i++
}
}
为什么不同写法结果会造成如此迥异的结果呢?两次collect都是针对同一个flow也就是number执行的操作,但是为什么第一种写法却各自安好互不干扰呢?这一切的秘密,都和Flow的创建流程有关。
flow的创建
注:本小节需要掌握基本的Kotlin知识,包括但不局限于高阶函数、扩展函数和内联函数等
不知你是否留意到如下代码中的秘密:
val numbers = flow {
emit(1)
}.flowOn(Dispatchers.IO)fun flowDemo() {
val numbers = flow {
println("this is:$this and hashcode is ${this.hashCode()}")
emit(1)
}.flowOn(Dispatchers.IO)
GlobalScope.launch(Dispatchers.IO) {
numbers.collect {
println("第一个:numbers is:$numbers and hashcode is ${numbers.hashCode()}")
}
}
GlobalScope.launch(Dispatchers.IO) {
numbers.collect {
println("第二个:numbers is:$numbers and hashcode is ${numbers.hashCode()}")
}
}
}
不难发现,numbers确实是同一个对象,但是flow代码块里的this
是什么,emit
方法是哪里定义的?为什么两个this
不是同一个对象?
这里为什么可以直接调用它呢?我们直接看一下到flow{}
底做了什么:
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)
private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() {
//注释①
override suspend fun collectSafely(collector: FlowCollector<T>) {
collector.block()
}
}
代码逻辑很简单:flow
方法接受了一个FlowCollector
的扩展函数,也就是我们定义的代码块。然后创建了一个SafeFlow
并返回。到这里,flow代码块里的this
和emit
方法的问题就得的了答案:this是FlowCollector
而emit
是它的成员函数。之所以可以在代码块里直接调用emit
,是因为扩展函数默认持有被扩展类实例对象的引用。它们在Kotlin中的定义如下:
public interface FlowCollector<in T> {
public suspend fun emit(value: T)
}
一个简单的接口。而这个扩展函数也就是我们所定义的代码块是在注释①,SafeFlow
的collectSafely
被调用的。collectSafely
接受一个FlowCollector
实例,并调用它的扩展方法。那么,关键问题是找到调用者和FlowCollector
创建的过程。这时,我们就要回头看一下collect
方法了:
public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
collect(object : FlowCollector<T> {
override suspend fun emit(value: T) = action(value)
})
很明显,这是一个内联函数,也是一个Flow
扩展函数。该扩展函数由调用了一个collect(collector: FlowCollector<T>)
方法,FlowCollector
就是在这里以匿名内部类的方式创建的。而在numbers.collect
中,numbers是SafeFlow
的实例。在上文中而它继承自AbstractFlow
:
public abstract class AbstractFlow<T> : Flow<T>, CancellableFlow<T> {
@InternalCoroutinesApi
public final override suspend fun collect(collector: FlowCollector<T>) {
val safeCollector = SafeCollector(collector, coroutineContext)
try {
collectSafely(safeCollector)
} finally {
safeCollector.releaseIntercepted()
}
}
public abstract suspend fun collectSafely(collector: FlowCollector<T>)
}
可以看到,collect
最终调用到了AbstractFlow.collect
方法,并创建了SafeCollector
的实例然后调用了collectSafely
方法,而collectSafely
是一个抽象函数,它的一个实现就是上文中的注释①处。至此,我们可以大致总结一下:
flow{}
接受一个FlowCollector
的扩展函数并返回SafeFlow
的一个实例。- 一旦调用
SafeFlow
的扩展函数collect
,collect
就会通过匿名内部类的方式创建一个FlowCollector
实例。并调用Flow.collect(collector: FlowCollector<T>)
方法。 Flow.collect
最终调用collectSafely
方法 。而该方法在SafeFlow
中的具体实现就是执行flow{}
代码块所定义的扩展函数。最终调用emit
方法开始发送数据
我们就不深入探讨SafeCollector
发送流程了。
到这里就可以解释上面多次collect
的行为了。虽然numbers都是同一个SafeFlow
的实例,但是真正执行emit
生成数据的FlowCollector
却不同,每次collect
都会生成不同的FlowCollector
总结
通过对flow{}
的解析,不难发现:SafeFlow
也就是Flow就是一个”工具人“,它的目的有以下几个关键作用:
- 提供用于生产数据的
FlowCollector
的扩展函数 - 接受用于响应数据的代码块,用来消费数据
- 在接受用于相应数据的代码块的同时,创建数据生产者
FlowCollector
并将生产者和消费者关联起来
SafeFlow
真的就只是个工具,用来将创建生产者和消费者并将它们关联在一起。这里也不难看出,flow是真的冷啊,collect
负责创建 FlowCollector
,也就是说如果没有消费者通过collect
和生产者关联,生产者压根就不存在,flow{}
代码块里的代码根本就不会执行,更不会有任何数据流产生!!