最近看了很多大佬关于LiveData和Flow比对的文章并做出了推荐,这里打个总结说下自己的理解。
首先我们应该要清楚liveData和flow的优缺点以及特性,才能在不同的场景到底应用liveData还是flow。
先说下liveData的优缺点:
- 观察者的回调永远发生在主线程,契合更新UI就发生在主线程;不仅回调发生在主线程,liveData设置数据和处理数据都发生在主线程,这就是个缺点了,无法处理背压问题
- 仅持有单个且最新的数据,这就不防抖,liveData传入相同的值也会被监控产生回调
- 自动取消订阅,因为liveData是有生命周期感知的;也是因为有感知生命周期的能力,liveData的粘性事件就是这么产生的。
- 提供「可读可写」和「仅可读」两个版本收缩权限
- 配合
DataBinding实现「双向绑定」
Transformation 和observer 都是工作在主线程,在我们的App中有这么一种情况,处理图表数据的时候同时也对数据进行监控,那么到底是observer在前还是switchMap先执行?
通过打印信息可以看到先进行observer的监控再进行switchMap方法的处理,这里可以使用flow进行优化,因为可能有性能问题。
总结:
-
LiveData作为一个 可感知生命周期的,可观察的,数据持有者,被设计用来更新 UI -
LiveData很轻,功能十分克制,克制到需要配合ViewModel使用才能显示其价值 -
由于
LiveData专注单一功能,因此它的一些方法使用上是有局限性的,即通过设计来强制开发者按正确的方式编码(如观察者仅在主线程回调,避免了开发者在子线程更新 UI 的错误操作)
Flow离不开协程,Kotlin 协程被用来处理异步任务,而 Flow 则是处理异步数据流。
回顾一下flow的实现原理:flow {} 方式(或flowOf, asFlow)创建的 Flow 实例是 SafeFlow 类型,其父类是 AbstractFlow 抽象类,当调用其 collect(FlowCollector) 方法时,首先会执行该 Flow 对象传入的 block 代码块,代码块中一般会有 emit 方法发射值,这个 emit 调用的就是 AbstractFlow.emit 方法,在其中做了安全判定后,会接着调用到 collect 中传入的 FlowCollector.emit 方法,对于 collect {} 的情况,emit 方法内部就是执行 collect 传入的 action 代码块。因为它在每次调用 collect 时才去触发发送数据的动作,所以说 Flow 是冷流。
flow优化代码方案:
private fun getUser() {
launchOnUI {
userDataRepository.getUser()
.onStart {
_userData.value = NetworkRequestUIState(isLoading = true)
}
.onEach { result ->
Logger.d("result = $result")
if (result is ResponseResult.Error) {
_userData.value =
NetworkRequestUIState(isLoading = false, isError = result.errorMsg)
} else if (result is ResponseResult.Success) {
_userData.value = NetworkRequestUIState(isLoading = false, isSuccess = result.data)
}
}
.commonCatch {
}
.collect()
}
}
将仓库中的接口方法优化成flow形式,viewModel和Repository使用Flow进行数据传输,_userData和userData仍然使用LiveData形式,viewModel和Activity使用LiveData进行数据传输,这也是现在比较推崇的做法。
为什么推崇这样的做法呢,这就说到上面liveData的特性
1.因为liveData是有生命周期感知的,适合使用在view层和viewModel层之间交互,flow是没有生命周期感知的,适合用在model层和viewModel层之间
2.liveData无法处理背压问题(就是在一段时间内发送数据的速度 > 接受数据的速度,LiveData 无法正确的处理这些请求)
3.liveData都放在主线程处理数据,对于线程控制不太理想
或者仓库中的接口方法不用变,在viewModel方法中直接使用flow进行处理也是一样的效果,使用中间运算符对接口请求的数据进行处理
fun getUpdateInfo() {
launchOnUI {
flow {
emit(mineRepository.getUpdateInfo("LowCarbon"))
}.onStart {
_updateData.value = NetworkRequestUIState(isLoading = true)
}.onEach { result ->
if (result is ResponseResult.Error) {
_updateData.value =
NetworkRequestUIState(isLoading = false, isError = result.errorMsg)
} else if (result is ResponseResult.Success) {
_updateData.value =
NetworkRequestUIState(isLoading = false, isSuccess = result.data)
}
}.commonCatch {}
.collect()
}
}
针对初始化的接口,比如加载站点树、获取用户信息等可以进行代码优化
仓库中的接口方法是flow形式,可以直接调用该方法并使用Flow的asLiveData()转换成LiveData,注意在activity中增加liveData的监听observer才会执行liveData{}代码块中的内容。
//用户基本信息
val userData = userDataRepository.getUser().map {
Logger.d("luxiang it = $it")
when(it) {
is ResponseResult.Error -> {
NetworkRequestUIState(isLoading = false,isSuccess = null, isError = it.errorMsg)
}
is ResponseResult.Success -> {
NetworkRequestUIState(isLoading = false, isSuccess = it.data)
}
}
}.asLiveData()
刚才说了在viewModel和Repository之间使用flow进行数据传递,viewModel和activity之间使用LiveData,其实效果已经还不错了,但是liveData单一的数据结构和特性无法满足复杂的数据传递,比如刚才说的数据默认不防抖、粘性事件,而热流就可以做到,此时shareFlow和StateFlow登场了。
SharedFlow和stateFLow和普通flow最大的区别就是,他们是热流,是热流,是热流,重要的事情说三遍。先来看下区别:
- 冷流:只有当订阅者发起订阅时,事件的发送者才会开始发送事件。
- 热流:不管订阅者是否存在,flow本身可以调用emit(或者tryEmit)发送事件,可以有多个观察者,也可在需要的时候发送事件。 从描述看,SharedFlow更接近于传统的观察者模式。
sharedflow和stateflow的关系:本质上,stateFlow就是一种特殊的sharedflow。
冷流用于获取数据和处理数据,热流用于传递数据(替代liveData的作用)
首先看一下ShareFlow的构造函数
@Suppress("FunctionName", "UNCHECKED_CAST")
public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
require(replay >= 0) { "replay cannot be negative, but was $replay" }
require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" }
require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) {
"replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow"
}
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)
}
其主要有3个参数
- reply:事件粘滞数,既当事件发送在订阅者订阅之前,会将订阅之前的第 i 份数据发送给这个新的订阅者。(类似的liveData粘滞事件就相当于reply=1,不同的是sharedflow可以自定义粘滞的数量)默认值=0,代表没有粘滞
- extraBufferCapacity:缓存容量,既然存在粘滞,就说明shardflow是有缓存的,缓存一方面用于粘滞事件的发送,另一方面主要是为了处理响应流中常见的背压问题,既当下游的订阅者collector消费速度低于上游生产速度(比如订阅者被挂起),数据流会被放在缓存中,缓存的大小就是由这个参数控制。
- onBufferOverflow:由背压就有处理策略,sharedflow默认为suspend,也即是如果当事件数量超过缓存,发送就会被挂起,其他还有drop_oldest和drop_latest
先不看SharedFlowImpl的具体实现,先来看下MutableSharedFlow接口的定义:
public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
override suspend fun emit(value: T)
public fun tryEmit(value: T): Boolean
public val subscriptionCount: StateFlow<Int>
@ExperimentalCoroutinesApi
public fun resetReplayCache()
}
-
FlowCollector: 是flow的统一接口,就是收集数据用的。
-
SharedFlow :有一个参数replayCache,看名字就是到了这个就是事件的粘滞列表(可以通过MutableSharedFlow.resetReplyCache来重置这个列表)。
-
emit:对sharedflow发送一条数据,如果buffer满了同时采用了suspend策略,emit方法会被挂起。
-
tryEmit:非挂起函数,如果发送成功了,返回true,如果失败则返回false。注意:当策略不采用suspend时,tryEmit永远返回true。(也就是说,只有当采用emit会被挂起的时候,采用tryEmit会返回false)
-
subscriptionCount 当前flow活动中的订阅者数量。(这个参数本身是一个stateFlow),官方还给我们提供了一个监听第一个订阅者出现以及最后一个订阅者消失的例子:
sharedFlow.subscriptionCount
.map { count -> count > 0 } // map count into active/inactive flag
.distinctUntilChanged() // only react to true<->false changes
.onEach { isActive -> // configure an action
if (isActive) onActive() else onInactive()
}
.launchIn(scope) // launch it
- resetReplayCache 重置粘滞缓存,新的订阅者只能获取新的事件,但是旧的订阅者(old subscriber)仍然会从缓存中获取旧的事件(因为缓存列表本身分为粘滞区和缓存区)
看完mutablesharedflow的接口定义,下面来看它的具体实现SharedFlowImpl。
private class SharedFlowImpl<T>(
private val replay: Int,
private val bufferCapacity: Int,
private val onBufferOverflow: BufferOverflow
) : AbstractSharedFlow<SharedFlowSlot>(), MutableSharedFlow<T>, CancellableFlow<T>, FusibleFlow<T> {
/*
Logical structure of the buffer
buffered values
/-----------------------\
replayCache queued emitters
/----------/----------------------\
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| | 1 | 2 | 3 | 4 | 5 | 6 | E | E | E | E | E | E | | | |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
^ ^ ^ ^
| | | |
head | head + bufferSize head + totalSize
| | |
index of the slowest | index of the fastest
possible collector | possible collector
| |
| replayIndex == new collector's index
---------------------- /
range of possible minCollectorIndex
head == minOf(minCollectorIndex, replayIndex) // by definition
totalSize == bufferSize + queueSize // by definition
INVARIANTS:
minCollectorIndex = activeSlots.minOf { it.index } ?: (head + bufferSize)
replayIndex <= head + bufferSize
*/
// Stored state
private var buffer: Array<Any?>? = null // allocated when needed, allocated size always power of two
private var replayIndex = 0L // minimal index from which new collector gets values
private var minCollectorIndex = 0L // minimal index of active collectors, equal to replayIndex if there are none
private var bufferSize = 0 // number of buffered values
private var queueSize = 0 // number of queued emitters
// Computed state
private val head: Long get() = minOf(minCollectorIndex, replayIndex)
private val replaySize: Int get() = (head + bufferSize - replayIndex).toInt()
private val totalSize: Int get() = bufferSize + queueSize
private val bufferEndIndex: Long get() = head + bufferSize
private val queueEndIndex: Long get() = head + bufferSize + queueSize
//重写的这个方法,关键就是replySize,而这个其实根据上图就是需要粘滞的数量的坐标值,既最后一位的缓存坐标-粘滞数量
override val replayCache: List<T>
get() = synchronized(this) {
val replaySize = this.replaySize
if (replaySize == 0) return emptyList()
val result = ArrayList<T>(replaySize)
val buffer = buffer!! // must be allocated, because replaySize > 0
@Suppress("UNCHECKED_CAST")
for (i in 0 until replaySize) result += buffer.getBufferAt(replayIndex + i) as T
result
}
//collect方法重点在于while(true),说明这个方法是会不停阻塞协程的,具体使用时,一个协程内第一个collect会阻塞接下来的代码,所以需要做异步处理
//会先尝试非阻塞获取,获取不到则使用挂起方式调用
@Suppress("UNCHECKED_CAST")
override suspend fun collect(collector: FlowCollector<T>) {
val slot = allocateSlot()
try {
if (collector is SubscribedFlowCollector) collector.onSubscription()
val collectorJob = currentCoroutineContext()[Job]
while (true) {
var newValue: Any?
while (true) {
// 首先尝试不挂起快速路径
newValue = tryTakeValue(slot)
if (newValue !== NO_VALUE) break
// 等待新值可用的信号
awaitValue(slot)
}
collectorJob?.ensureActive()
collector.emit(newValue as T)
}
} finally {
freeSlot(slot)
}
}
//tryEmit主要是根据tryEmitLocked的结果返回
override fun tryEmit(value: T): Boolean {
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
val emitted = synchronized(this) {
if (tryEmitLocked(value)) {
resumes = findSlotsToResumeLocked(resumes)
true
} else {
false
}
}
for (cont in resumes) cont?.resume(Unit)
return emitted
}
//会先进行一次tryEmit的处理,当返回false的时候再进行suspend的发送操作
override suspend fun emit(value: T) {
if (tryEmit(value)) return // fast-path
emitSuspend(value)
}
//这里可以看到,当策略采用suspended同时缓存溢出的时候,返回false,否则,永远返回true,同时做一些事件上的入队处理等
@Suppress("UNCHECKED_CAST")
private fun tryEmitLocked(value: T): Boolean {
// Fast path without collectors -> no buffering
if (nCollectors == 0) return tryEmitNoCollectorsLocked(value) // always returns true
// With collectors we'll have to buffer
// cannot emit now if buffer is full & blocked by slow collectors
if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) {
when (onBufferOverflow) {
BufferOverflow.SUSPEND -> return false // will suspend
BufferOverflow.DROP_LATEST -> return true // just drop incoming
BufferOverflow.DROP_OLDEST -> {} // force enqueue & drop oldest instead
}
}
enqueueLocked(value)
bufferSize++ // value was added to buffer
// drop oldest from the buffer if it became more than bufferCapacity
if (bufferSize > bufferCapacity) dropOldestLocked()
// keep replaySize not larger that needed
if (replaySize > replay) { // increment replayIndex by one
updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex)
}
return true
}
//可以看到,在内部再一次调用了tryEmitLocked()的方法,来再次确认队列是否满了
//emit方法可以理解成先使用tryEmit进行发送,如果发送失败,则将emitter加入到队列中
private suspend fun emitSuspend(value: T) = suspendCancellableCoroutine<Unit> sc@{ cont ->
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
val emitter = synchronized(this) lock@{
// recheck buffer under lock again (make sure it is really full)
if (tryEmitLocked(value)) {
cont.resume(Unit)
resumes = findSlotsToResumeLocked(resumes)
return@lock null
}
// add suspended emitter to the buffer
Emitter(this, head + totalSize, value, cont).also {
enqueueLocked(it)
queueSize++ // added to queue of waiting emitters
// synchronous shared flow might rendezvous with waiting emitter
if (bufferCapacity == 0) resumes = findSlotsToResumeLocked(resumes)
}
}
// outside of the lock: register dispose on cancellation
emitter?.let { cont.disposeOnCancellation(it) }
// outside of the lock: resume slots if needed
for (cont in resumes) cont?.resume(Unit)
}
下面说一下stateFlow,stateFlow其实就是一种特殊的SharedFlow:
public interface StateFlow<out T> : SharedFlow<T> {
/**
* The current value of this state flow.
*/
public val value: T
}
@Suppress("FunctionName")
public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)
public interface MutableStateFlow<T> : StateFlow<T>, MutableSharedFlow<T> {
public override var value: T
public fun compareAndSet(expect: T, update: T): Boolean
}
在sharedFlow的基础上加入了一个value(类似liveData),新增了一个初始值,增加一个防数据抖动的判断函数(更新数值的时候比较当前值,如果相同则不更新)
private class StateFlowImpl<T>(
initialState: Any // T | NULL
) : AbstractSharedFlow<StateFlowSlot>(), MutableStateFlow<T>, CancellableFlow<T>, FusibleFlow<T> {
...
override val replayCache: List<T>
get() = listOf(value)
override fun tryEmit(value: T): Boolean {
this.value = value
return true
}
override suspend fun emit(value: T) {
this.value = value
}
@Suppress("UNCHECKED_CAST")
override fun resetReplayCache() {
throw UnsupportedOperationException("MutableStateFlow.resetReplayCache is not supported")
}
...
}
从这里可以看到,stateFlow就是一个replaySize=1的sahredFlow,同时不能调用它的resetRecycleCache方法否则会报错。
SharedFlow和StateFlow的侧重点
- StateFlow就是一个replaySize=1的sharedFlow,同时它必须有一个初始值,此外,每次更新数据都会和旧数据做一次比较,只有不同时候才会更新数值。
- StateFlow重点在状态,ui永远有状态,所以StateFlow必须有初始值,同时对ui而言,过期的状态毫无意义,所以stateFLow永远更新最新的数据(和liveData相似),所以必须有粘滞度=1的粘滞事件,让ui状态保持到最新。具体应用时,StateFlow适合那些长期保持某种状态的ui,比如一些开关值之类。
- SharedFlow侧重在事件,当某个事件触发,发送到队列之中,按照挂起或者非挂起、缓存策略等将事件发送到接受方,在具体使用时,SharedFlow更适合通知ui界面的一些事件,比如toast等,也适合作为viewModel和repository之间的桥梁用作数据的传输。
将冷流转化为热流
使用官方提供的shareIn方法:
val myFlow = flow<String> { }.shareIn(
scope = viewModelScope,//作用域
started = SharingStarted.WhileSubscribed(),//启动策略
replay = 0//粘滞度
)
SharingStarted.WhileSubscribed():存在订阅者时,将使上游提供方保持活跃状态。SharingStarted.Eagerly:立即启动提供方。SharingStarted.Lazily:在第一个订阅者出现后开始共享数据,并使数据流永远保持活跃状态。