Flow

292 阅读8分钟

一、为什么要使用 Flow?

LiveData 是 androidx 包下的组件,是 Android 生态中一个的简单的生命周期感知型容器。简单即是它的优势,也是它的局限,当然这些局限性不应该算 LiveData 的缺点,因为 LiveData 的设计初衷就是一个简单的数据容器。对于简单的数据流场景,使用 LiveData 完全没有问题。

LiveData 有以下几个局限性:

  • LiveData 只能在主线程更新数据: 只能在主线程 setValue,即使 postValue 内部也是切换到主线程执行;

  • LiveData 数据重放问题: 注册新的订阅者,会重新收到 LiveData 存储的数据,这在有些情况下不符合预期(可以使用自定义的 LiveData 子类 SingleLiveData解决);

  • LiveData 不防抖: 重复 setValue 相同的值,订阅者会收到多次 onChanged() 回调(可以使用 distinctUntilChanged() 解决);

  • LiveData 不支持背压: 在数据生产速度 > 数据消费速度时,LiveData 无法正常处理。比如在子线程大量 postValue 数据但主线程消费跟不上时,中间就会有一部分数据被忽略。

因此推出Flow,Flow的特点如下:

  • Flow 支持协程: Flow 基于协程基础能力,能够以结构化并发的方式生产和消费数据,能够实现线程切换(依靠协程的 Dispatcher);
  • Flow 支持数据重放配置: Flow 的子类 SharedFlow 支持配置重放 replay,能够自定义对新订阅者重放数据的配置;
  • Flow 防抖: Flow 的子类 StateFlow 支持数据防抖,意味着仅在更新值并且发生变化才会回调,如果更新值没有变化不会回调 collect,其实就是在发射数据时加了一层拦截。
  • Flow 支持背压: Flow 的子类 SharedFlow 支持配置缓存容量,可以应对数据生产速度 > 数据消费速度的情况;
  • Flow 不是生命周期感知型组件: Flow 不是 Android 生态下的产物,自然 Flow 是不会关心组件生命周期。那么我们如何确保订阅者在监听 Flow 数据流时,不会在错误的状态更新 View 呢?Google 推荐的做法是使用 Lifecycle#repeatOnLifecycle

二、冷数据流 Flow

冷流有以下几个特点:

  • 冷流是不共享的,也没有缓存机制;
  • 冷流只有在订阅者 collect 数据时,才按需执行发射数据流的代码,并且每次 collect 都会创建一个全新的数据流;
  • 冷流和订阅者是一对一的关系,多个订阅者间的数据流是相互独立的,一旦订阅者停止监听或者生产代码结束,数据流就自动关闭。

下面看个例子:

fun testFlow(){
    val scope = CoroutineScope(Dispatchers.IO+ Job())

    scope.launch {
        println("scope launch")
        val flow = flow<Int> {
            println("start emit")
            emit(100)
            println("end emit")
        }

        delay(2000)
        println("start collect")
        flow.collect{
            println("collect result : $it")
        }
        
        println("start collect second")
        flow.collect{
            println("collect result second : $it")
        }
    }
}

输出:

scope launch
start collect
start emit
collect result : 100
end emit
start collect second
start emit
collect result second : 100
end emit

三、热数据流 SharedFlow

SharedFlow 和 StateFlow 都属于热流,它们都有一个可变的版本 MutableSharedFlow 和 MutableStateFlow,这与 LiveData 和 MutableLiveData 类似,对外暴露接口时,应该使用不可变的版本。

热流相较于冷流,热流无论是否有订阅者(collect),都可以生产数据并且缓存。

3.1、MutableSharedFlow

看下 MutableSharedFlow 源码:

public fun <T> MutableSharedFlow(
    // 重放数据个数
    replay: Int = 0,
    // 额外缓存容量
    extraBufferCapacity: Int = 0,
    // 缓存溢出策略
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
    val bufferCapacity0 = replay + extraBufferCapacity
    val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow
    return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow)
}

public enum class BufferOverflow {
    // 挂起
    SUSPEND,
    // 丢弃最早的一个
    DROP_OLDEST,
    // 丢弃最近的一个
    DROP_LATEST
}

MutableSharedFlow 构造函数允许我们配置三个参数:

参数描述
reply重放数据个数,当新订阅者时注册时会重放缓存的 replay 个数据
extraBufferCapacity额外缓存容量,在 replay 之外的额外容量,SharedFlow 的缓存容量 capacity = replay + extraBufferCapacity
onBufferOverflow缓存溢出策略,即缓存容量 capacity 满时的处理策略(SUSPEND、DROP_OLDEST、DROP_LAST)

SharedFlow 默认容量 capacity 为 0,重放 replay 为 0,缓存溢出策略是 SUSPEND,发射数据时已注册的订阅者会收到数据,但数据会立刻丢弃,而新的订阅者不会收到历史发射过的数据。

3.2、MutableSharedFlow 转为 SharedFlow

public fun <T> MutableSharedFlow<T>.asSharedFlow(): SharedFlow<T> =
    ReadonlySharedFlow(this, null)

3.3、Flow 转为 SharedFlow

前面提到过,冷流是不共享的,也没有缓存机制。使用 Flow.shareIn 可以把冷流转换为热流,一来可以将数据共享给多个订阅者,二来可以增加缓冲机制。

public fun <T> Flow<T>.shareIn(
    // 协程作用域范围
    scope: CoroutineScope,
    // 启动策略
    started: SharingStarted,
    // 控制数据重放的个数
    replay: Int = 0
): SharedFlow<T> {
  val config = configureSharing(replay)
  val shared = MutableSharedFlow<T>(
      replay = replay,
      extraBufferCapacity = config.extraBufferCapacity,
      onBufferOverflow = config.onBufferOverflow
  )
  @Suppress("UNCHECKED_CAST")
  scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
  return shared.asSharedFlow()
}
public companion object {
    // 热启动式:立即开始,并在 scope 指定的作用域结束时终止
    public val Eagerly: SharingStarted = StartedEagerly()
    // 懒启动式:在注册首个订阅者时开始,并在 scope 指定的作用域结束时终止
    public val Lazily: SharingStarted = StartedLazily()
 
    public fun WhileSubscribed(
        stopTimeoutMillis: Long = 0,
        replayExpirationMillis: Long = Long.MAX_VALUE
    ): SharingStarted =
        StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)
}

sharedIn 的参数 scope 和 replay 不需要过多解释,主要介绍下 started: SharingStarted 启动策略,分为三种:

参数描述
Eagerly(热启动式)立即启动数据流,并保持数据流(直到 scope 指定的作用域结束)
Lazily(懒启动式)在首个订阅者注册时启动,并保持数据流(直到 scope 指定的作用域结束)
WhileSubscribed()在首个订阅者注册时启动,并保持数据流直到在最后一个订阅者注销时结束(或直到 scope 指定的作用域结束)

通过 WhildSubscribed() 策略能够在没有订阅者的时候及时停止数据流,避免引起不必要的资源浪费,例如一直从数据库、传感器中读取数据。

whileSubscribed() 还提供了两个配置参数:

  • stopTimeoutMillis 超时时间(毫秒): 最后一个订阅者注销订阅后,保留数据流的超时时间,默认值 0 表示立刻停止。这个参数能够帮助防抖,避免订阅者临时短时间注销就马上关闭数据流。例如希望等待 5 秒后没有订阅者则停止数据流,可以使用 whileSubscribed(5000)。
  • replayExpirationMillis 重放过期时间(毫秒): 停止数据流后,保留重放数据的超时时间,默认值 Long.MAX_VALUE 表示永久保存(replayExpirationMillis 发生在停止数据流后,说明 replayExpirationMillis 时间是在 stopTimeoutMillis 之后发生的)。例如希望希望等待 5 秒后停止数据流,再等待 5 秒后的数据视为无用的陈旧数据,可以使用 whileSubscribed(5000, 5000)。

四、热数据流 StateFlow

StateFlow 是 SharedFlow 的子接口,可以理解为一个特殊的 SharedFlow。

4.1、MutableStateFlow

看下 MutableStateFlow 源码:

public interface MutableStateFlow<T> : StateFlow<T>, MutableSharedFlow<T> {
    // 当前值
    public override var value: T

    // 比较并设置(通过 equals 对比,如果值发生真实变化返回 true)
    public fun compareAndSet(expect: T, update: T): Boolean
}

MutableStateFlow 的构造函数就简单多了,有且仅有一个必选的参数,代表初始值:

public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

StateFlow 是 SharedFlow 的一种特殊配置,MutableStateFlow(initialValue) 这样一行代码本质上和下面使用 SharedFlow 的方式是完全相同的:

val shared = MutableSharedFlow(
    replay = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)
shared.tryEmit(initialValue) // emit the initial value
val state = shared.distinctUntilChanged() // get StateFlow-like behavior
  • 有初始值: StateFlow 初始化时必须传入初始值;

  • 容量为 1: StateFlow 只会保存一个值;

  • 重放为 1: StateFlow 会向新订阅者重放最新的值;

  • 不支持 resetReplayCache() 重置重放缓存: StateFlow 的 resetReplayCache() 方法抛出 UnsupportedOperationException

  • 缓存溢出策略为 DROP_OLDEST: 意味着每次发射的新数据会覆盖旧数据。

  • 数据防抖: 意味着仅在更新值并且发生变化才会回调,如果更新值没有变化不会回调 collect,其实就是在发射数据时加了一层拦截compareAndSet

4.2、MutableStateFlow 转为 StateFlow

public fun <T> MutableStateFlow<T>.asStateFlow(): StateFlow<T> =
    ReadonlyStateFlow(this, null)

4.3、Flow 转为 StateFlow

跟 SharedFlow 一样,普通 Flow 也可以转换为 StateFlow。

public fun <T> Flow<T>.stateIn(
    // 共享开始时所在的协程作用域范围
    scope: CoroutineScope,
    // 共享开始策略
    started: SharingStarted,
    // 初始值
    initialValue: T
): StateFlow<T> {
    val config = configureSharing(1)
    val state = MutableStateFlow(initialValue)
    scope.launchSharing(config.context, config.upstream, state, started, initialValue)
    return state.asStateFlow()
}

五、Flow 生命周期安全问题

前面也提到了,Flow 不具备 LiveData 的生命周期感知能力,所以订阅者在监听 Flow 数据流时,会存在生命周期安全的问题。Google 推荐的做法是使用 Lifecycle#repeatOnLifecycle

方法描述
Activity.lifecycleScope.launch立即启动协程,并在 Activity 销毁时取消协程
Fragment.lifecycleScope.launch立即启动协程,并在 Fragment 销毁时取消协程
Fragment.viewLifecycleOwner.lifecycleScope.launch立即启动协程,并在 Fragment 中视图销毁时取消协程
LifecycleContinueScope.launchWhenX在生命周期到达指定状态时立即启动协程执行代码块,在生命周期低于该状态时挂起(而不是取消)协程,在生命周期重新高于指定状态时,自动恢复该协程
Lifecycle.repeatOnLifecycle在生命周期到达指定状态时立即启动协程执行代码块,在生命周期低于该状态时取消协程,在生命周期重新高于指定状态时,自动启动该协程。
Flow.flowWithLifecycle内部基于 Lifecycle.repeatOnLifecycle

可以看到:

  • 1 - 3:这些协程 API 只有在最后组件 / 视图销毁时才会取消协程,当视图进入后台时协程并不会被取消,Flow 会持续生产数据,并且会触发更新视图;
  • 4:这些协程 API 在视图离开某个状态时会挂起协程,能够避免更新视图。但是 Flow 会持续生产数据,也会产生一些不必要的操作和资源消耗(CPU 和内存);
  • 5 - 6:很好的避免了一些不必要的操作和资源消耗(CPU 和内存)。

repeatOnLifecycle 常见用法:

lifecycleScope.launch { 
    repeatOnLifecycle(Lifecycle.State.STARTED){
		
    }
}

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {

    }
}

回过头来看,repeatOnLifecycle 是怎么实现生命周期感知的呢?其实很简单,是通过 Lifecycle#addObserver 来监听生命周期变化。

suspendCancellableCoroutine<Unit> { cont ->
    // Lifecycle observers that executes `block` when the lifecycle reaches certain state, and
    // cancels when it falls below that state.
    val startWorkEvent = Lifecycle.Event.upTo(state)
    val cancelWorkEvent = Lifecycle.Event.downFrom(state)
    val mutex = Mutex()
    observer = LifecycleEventObserver { _, event ->
        if (event == startWorkEvent) {
            // Launch the repeating work preserving the calling context
            launchedJob = this@coroutineScope.launch {
                // Mutex makes invocations run serially,
                // coroutineScope ensures all child coroutines finish
                mutex.withLock {
                    coroutineScope {
                        block()
                    }
                }
            }
            return@LifecycleEventObserver
        }
        if (event == cancelWorkEvent) {
            launchedJob?.cancel()
            launchedJob = null
        }
        if (event == Lifecycle.Event.ON_DESTROY) {
            cont.resume(Unit)
        }
    }
    this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver)
}