协程之 Flow

1,212 阅读5分钟

核心原理

Flow 中最重要的函数一共有三个:emit,collect, flow。其中 flow 最简单,它就是一工厂方法,生产并返回 Flow 对象。不使用 flow 函数直接 new Flow 效果也一样。

使用这三个函数可以构建一个最基本的 flow 使用

val f = flow<Int> { // lambda 1
    emit(1)
}
f.collect { // lambda 2
    Log.e(TAG, "onCreate: $it")
}

上面代码表明了 Flow 最核心的执行流程是:调用 collect 会触发 lambda 1 的执行,调用 emit 会触发 lambda 2 的执行。这是 Flow 最最核心的流程。

如果 debug 跟代码可以发现,collect 直接调用了 lambda 1,而 emit 直接调用了 lambda 2。它们就是普普通通的方法调用,没有任何神奇操作。因此 可以直接 try-catch 住对方抛出的异常,这部分可参考 catch 操作符原理,这就是操作符 first 函数的底层实现原理:它通过抛出一个特殊异常中止了整个 emit 流程.

我们常说 Flow 是冷流,只有在被使用时才会调用它的生产函数(即上面代码中的 lambda 1)。原理很简单:如果没有调用 collect(),那么 lambda 1 只是被定义而没有被调用,当然不会执行,也就不会生产数据。

常见误区

  1. Flow 是同步的,它不是异步的。一般 Flow 会用在生产者消费者中,它的同步说明生产者和消费者运行在同一线程。消费者调用 collect 方法,collect() 又是方法调用形式一层层往上调用到最初生产者,最初生产者又通过 emit 一层层往下传递到消费者,整个过程并不会切换线程。

  2. 虽然 flow 是同步的,但 flow 本身要运行在协程之中,因此它不一定运行在主线程中。collect 是 suspend 函数只能运行在协程中,而 flow 的整个传递链路是普通方法调用,所以最初生产者、操作符都会在某个协程中被调用。比如下面代码

    private fun usersFlow(): Flow<String> = flow {
        repeat(2) {
            // currentCoroutineContext 是 suspend 函数
            // 但整个 usersFlow 并没有任何协程相关的配置,依旧可以调用 suspend 函数
            // 就这说明 flow 整个运行在某个协程中
            val ctx = currentCoroutineContext()
            val name = ctx[CoroutineName]?.name
            emit("User$it in $name")
        }
    }
    

flow, channelFlow 与 callbackFlow

三者都是 Flow 的工厂方法,返回 Flow 对象。而 Flow 是冷流,所以只有消费者订阅时才开始生产数据

  1. flow 返回的 Flow 是同步的,即消费者与生产者相当于方法直接调用,不存在线程切换。lambda 表达式是 FlowCollector 的扩展函数,调用的 emit 就是 FlowCollector 中的 emit 方法
  2. channelFlow: lambda 表达式是 ProducerScope 的扩展函数,它是 CoroutineScope 的子类,所以可以启动协程,也就是说生产者与消费者可处于不同线程。当然 channelFlow 了可当作普通的 flow 使用,使用效果与 flow 完全一样
  3. callbackFlow 用于将 callback 转换成 flow,callback 单次的使用挂起函数,多次的才使用 flow

如下代码虽然用了 channelFlow 但并没有切换线程,所以消费者中的 delay 依旧会阻塞生产中,即使有 buffer。

channelFlow<Int> {
    repeat(10) {
        send(Random(System.currentTimeMillis()).nextInt())
    }
}.buffer(10).collect {
    delay(2000)
    Log.e(TAG, "collect: ${Thread.currentThread()}  $it")
}

同样的代码,只要将 channelFlow 中代码换成如下代码,可以发现生产者会很迅速地生产完所有数据,但消费者还是隔 2s 消费一次。

withContext(Dispatchers.Default) {
    repeat(10) {
        Log.e(TAG, "sendResult: ${Thread.currentThread()}")
        scope.send(Random(System.currentTimeMillis()).nextInt())
    }
}

从上面两段代码可以很明显地看出 flow 与 channelFlow 的区别:前者用于同步生产,后者用于异步(因为可用协程)

flowOn

切换上游运行的协程环境

上面说过,flow 的所有代码都运行在某个协程中,既然是协程那么然有 CoroutineContext,也必须可以修改某中某些元素。这就是 flowOn 操作符的意义。

onEmpty

指的是它的直接上游是否生产有数据,而不是最初的生产者是否生产有数据

  • onEmpty 中可以调用 emit 往下游发送数据。能否调用 emit 往下游发送数据主要看 lambda 表达式是不是 FlowCollector 的扩展函数。下面 catch, onCompletion 都可以调用 emit。

示例

如下代码,其中第一段代码中 onEmpty 会执行。虽然最初生产者 (lambda 1) 生产有数据,但被 filter 过滤了,所以 onEmpty 的直接上游是没有数据的,就会执行

第二段代码中第二个 onEmpty 不会执行,因为它的直接上游 onEmpty 有数据产生,虽然最初的生产者没有生产数据。

// 第一段代码
flow<Int> { // lambda 1
    emit(1)
}.filter {
    it >= 2
}.onEmpty {
    Log.e(TAG, "onCreate: onEmpty")
}.collect { // lambda 2
    Log.e(TAG, "onCreate: $it")
}

// 第二段代码
flow<Int> {
    
}.onEmpty {
    emit(1) // 生产有数据,所以第二个 onEmpty 不会执行
}.onEmpty {
    emit(2)
}.collect {
    Log.e(TAG, "onCreate: collect $it")
}

源码

onEmpty 的内部执行逻辑如下:下游调用 onEmpty() 返回的 Flow 对象的 collect() 方法,就会调用到 unsafeFlow() 返回值的 collect() 方法,进而触发 onEmpty 中 lambda 1 的执行,而 lambad 1 中的参数 this 也指向了下游传入的对象(一般就是 collect() 函数后面跟的 lambda 表达式)。

public fun <T> Flow<T>.onEmpty(
    action: suspend FlowCollector<T>.() -> Unit
): Flow<T> = unsafeFlow { this: FlowCollector -> // lambda 1
    var isEmpty = true
    
    // 调用上游的 collect 方法,从而触发上游生产数据 
    collect { it: T ->
        isEmpty = false
        
        // 调用 emit,将数据又传递给下游
        // 调用 emit() 方法的对象就是 lambda 1 中的 this
        // 也即下游 collect() 中定义的 lambda 表达式
        emit(it)
    }
    
    // collect 是一个 suspend 方法,执行到此步时说明上游生产者已将所有数据发送下去
    // 所以 isEmpty 就可以表示上游是否给了数据
    
    if (isEmpty) {
        val collector = SafeCollector(this, currentCoroutineContext())
        try {
            collector.action()
        } finally {
            collector.releaseIntercepted()
        }
    }
}

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()
        }
    }
}

onCompletion

整个 flow 结束时的回调

示例

  • 它与 onEmpty 最大的区别在于:onEmpty 只关注它的直接上游是否产生有数据,而 onCompletion 关注整个 flow 链

  • 如果有多个 onCompletion,这些 onCompletion 会顺序执行

  • 无论 onCompletion 位于何处,它都是 flow 链中最后一环,而且从上游传下来的数据并不会经过它,但它调用的 emit 会传递给 collect 或其它终止操作符

如下代码:

flow<Int> {
}.onEmpty {
    Log.e(TAG, "onCreate: empty")
    emit(3)
}.onCompletion {
    Log.e(TAG, "onCreate: onCompletion 1 ")
    emit(1)
}.onCompletion {
    Log.e(TAG, "onCreate: onCompletion 2")
    emit(2)
}.collect {
    Log.e(TAG, "onCreate: collect $it")
}

// output

onCreate: empty   // 因为 flow{} 中没有数据,所以 onEmpty 最先执行
onCreate: collect 3 // onEmpty 中 emit(3),直接传递到最终的 collect 中
onCreate: onCompletion 1   // 整个流程结束,所以回调 onComplete
onCreate: collect 1      // 第一个 onComplete 调用 emit() 触发最后 collect 的执行
onCreate: onCompletion 2  // 第二个 onComplete 被触发
onCreate: collect 2    // 第二个 onComplete 中调用了 emit(),触发最后 collect 执行

源码

onCompletion 内部逻辑:直接将下游的消费者(FlowCollector 对象)传递给上游,所以上游调用 emit() 时就直接到达它的下游,因此所有的数据并不会经过 onCompletion。

public fun <T> Flow<T>.onCompletion(
    action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit
): Flow<T> = unsafeFlow { this : FlowCollector ->
    try {
        // 调用的是 onCompletion 的所属者
        // 传递的是 this,所以上游调用的 emit() 就是 this 中的 emit
        // 数据就会直达 onCompletion 的下游
        collect(this)
    } catch (e: Throwable) {
        // ....
    }

    // 正常回调
    // 如果 action 里面调用了 emit(),相当于调用 sc 的 emit
    // 而 sc 最终又转交给 this
    // 所以 onCompletion 中的 emit() 也会到下游中
    val sc = SafeCollector(this, currentCoroutineContext())
    try {
        sc.action(null)
    } finally {
        sc.releaseIntercepted()
    }
}

catch

捕获上游 flow 链中的任何异常

使用

catch 只能捕获上游异常,无法捕获下游异常。因此如果在 collect{} 中出现异常,catch 无能为力 —— 因为 collect 是终止操作符,catch 无法在它后面调用。

如果需要捕获 collect{} 中的异常,可以将 collect{} 中的操作移动到 onEach{} 中,在 onEach{} 后面调用 catch。如下:

    flow.onStart { println("Before") }
        .onEach { 
            // 原 collect 中的逻辑
        }
        .catch { 
            // 异常处理逻辑
        }
        // 空 collect,触发生产者开始生产数据
        // 这个是必须有的
        .collect()

源码

看 catch 源码主要看 flow 中关于异常的处理逻辑

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)
    }
    
internal suspend fun <T> Flow<T>.catchImpl(
    collector: FlowCollector<T>
): Throwable? {
    var fromDownstream: Throwable? = null
    try {
        collect {
            try {
                // 捕获 emit 异常,保证下游异常也能被捕获
                collector.emit(it)
            } catch (e: Throwable) {
                // 此处重新抛出下游异常,会被外面 catch 住 
                fromDownstream = e
                throw e
            }
        }
    } catch (e: Throwable) {
        val fromDownstream = fromDownstream
        // 如果是下游异常,重新抛一次
        // 如果是取消的,也要重新抛出次
        if (e.isSameExceptionAs(fromDownstream) || e.isCancellationCause(coroutineContext)) {
            throw e // Rethrow exceptions from downstream and cancellation causes
        } else {
            // 如果下游没发生异常,那就是上游发生了异常,将异常返回
            // 并最终传给 catch
            if (fromDownstream == null) {
                return e
            }
            // 如果下游有异常,都需要抛出去,取消整个 flow 链
            
            if (e is CancellationException) {
                fromDownstream.addSuppressed(e)
                throw fromDownstream
            } else {
                e.addSuppressed(fromDownstream)
                throw e
            }
        }
    }
    return null
}

dropWhile

丢失所有元素,直到第一个不满足条件的元素出现。因此,可以实现某些元素的首先出现

看名字叫 dropUntil 是不是更合适些。在 Flow::shareIn() 的源码中,调用过程中有如下代码。其中使用了 dropWhile 表示直到 it == start 时才放行,保证了某个元素的最先位置。

image.png

其源码很简单

image.png

stateIn 与 shareIn

将普通 Flow 转换成 StateFlow 与 SharedFlow

两者都需要 CoroutineScope 类型的参数,用于指定上游生产者运行的协程环境,这一点与 launchIn 操作符作用一样。以 stateIn 为例说明源码

image.png

其它操作符

省略