Kotlin flow 的创建原理和流程

881 阅读6分钟

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代码块里的thisemit方法的问题就得的了答案:this是FlowCollectoremit是它的成员函数。之所以可以在代码块里直接调用emit,是因为扩展函数默认持有被扩展类实例对象的引用。它们在Kotlin中的定义如下:

public interface FlowCollector<in T> {
    public suspend fun emit(value: T)
}

一个简单的接口。而这个扩展函数也就是我们所定义的代码块是在注释①,SafeFlowcollectSafely被调用的。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 的扩展函数collectcollect就会通过匿名内部类的方式创建一个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{}代码块里的代码根本就不会执行,更不会有任何数据流产生!!