1.LiveData 是粘性的吗?若是,它是怎么做到的?
LiveData粘性的特点:
在 Android 的 Architecture Components 里,LiveData 有一个“粘性”(sticky)的特点,主要表现为:
- 保持最新值(缓存特性)
LiveData 会存储最近一次调用 setValue() 或 postValue() 发布的数据。
当有新的观察者(Observer)订阅并进入「活跃」状态时,马上就会收到这条最新的值通知,即便这次订阅发生在数据更新之后。
- 跨越「订阅–取消订阅」周期
如果一个 LifecycleOwner(如 Activity/Fragment)因为配置变化(旋转、回收重建)或页面切换而短暂取消订阅(onStop() 后进入非活跃),然后又重新进入活跃状态(onStart()/onResume()),它依然会立刻收到那条“粘”在 LiveData 上的最新值。
- 保证 UI 与数据一致
这种粘性使得 UI 层在重建后,不必手动再去主动拉取一次数据;只要重新绑定 LiveData,就能自动拿到「当前」状态,降低了样板代码。
LiveData是粘性的原因:
LiveData 的值被存储在内部的字段中,直到有更新的值覆盖,所以值是持久的。 两种场景下 LiveData 会将存储的值分发给观察者。一是值被更新,此时会遍历所有观察者并分发之。二是新增观察者或观察者生命周期发生变化(至少为 STARTED),此时只会给单个观察者分发值。 LiveData 的观察者会维护一个“值的版本号”,用于判断上次分发的值是否是最新值。该值的初始值是-1,每次更新 LiveData 值都会让版本号自增。 LiveData 并不会无条件地将值分发给观察者,在分发之前会经历三道坎:1. 数据观察者是否活跃。2. 数据观察者绑定的生命周期组件是否活跃。3. 数据观察者的版本号是否是最新的。 “新观察者”被“老值”通知的现象叫“粘性”。因为新观察者的版本号总是小于最新版号,且添加观察者时会触发一次老值的分发。
2.粘性的 LiveData 会造成什么问题?怎么解决?
购物车-结算场景:假设有一个购物车界面,点击结算后跳转到结算界面,结算界面可以回退到购物车界面。这两个界面都是 Fragment。
结算界面和购物车界面通过共享ViewModel的方式共享商品列表:
class MyViewModel:ViewModel() {
// 商品列表
val selectsListLiveData = MutableLiveData<List<String>>()
// 更新商品列表
fun setSelectsList(goods:List<String>){
selectsListLiveData.value = goods
}
}
下面是俩 Fragment 界面依托的 Activity
class StickyLiveDataActivity : AppCompatActivity() {
// 用 DSL 构建视图
private val contentView by lazy {
ConstraintLayout {
layout_id = "container"
layout_width = match_parent
layout_height = match_parent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
// 加载购物车界面
supportFragmentManager.beginTransaction()
.add("container".toLayoutId(), TrolleyFragment())
.commit()
}
}
购物车页面如下:
class TrolleyFragment : Fragment() {
// 获取与宿主 Activity 绑定的 ViewModel
private val myViewModel by lazy {
ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
// 向购物车添加两件商品
onClick = {
myViewModel.setSelectsList(listOf("meet","water"))
}
TextView {
layout_id = "balance"
layout_width = wrap_content
layout_height = wrap_content
text = "balance"
gravity = gravity_center
// 跳转结算页面
onClick = {
parentFragmentManager.beginTransaction()
.replace("container".toLayoutId(), BalanceFragment())
.addToBackStack("trolley")
.commit()
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 观察商品列表变化
myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { goods ->
// 若商品列表超过2件商品,则 toast 提示已满
goods.takeIf { it.size >= 2 }?.let {
Toast.makeText(context,"购物车已满",Toast.LENGTH_LONG).show()
}
}
}
}
在 onViewCreated() 中观察购物车的变化,如果购物车超过 2 件商品,则 toast 提示。
下面是结算页面:
class BalanceFragment:Fragment() {
private val myViewModel by lazy {
ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 结算界面获取购物列表的方式也是观察商品 LiveData
myViewModel.selectsListLiveData.observe(viewLifecycleOwner) {...}
}
}
跑一下 demo,当跳转到结算界面后,点击返回购物车,toast 会再次提示购物车已满。
因为在跳转结算页面之前,购物车列表 LiveData 已经被更新过。当购物车页面重新展示时,onViewCreated() 会再次执行,这样一个新观察者被添加,因为 LiveData 是粘性的,所以上一次购物车列表会分发给新观察者,这样 toast 逻辑再一次被执行。
解决方案之一,使用SingleLiveEvent
这是谷歌给出的一个解决方案,源码可以点击这里
public class SingleLiveEvent<T> extends MutableLiveData<T> {
// 标志位,用于表达值是否被消费
private final AtomicBoolean mPending = new AtomicBoolean(false);
public void observe(LifecycleOwner owner, final Observer<T> observer) {
// 中间观察者
super.observe(owner, new Observer<T>() {
@Override
public void onChanged(@Nullable T t) {
// 只有当值未被消费过时,才通知下游观察者
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}
}
});
}
public void setValue(@Nullable T t) {
// 当值更新时,置标志位为 true
mPending.set(true);
super.setValue(t);
}
public void call() {
setValue(null);
}
}
专门设立一个 LiveData,它不具备粘性。它通过新增的“中间观察者”,拦截上游数据变化,然后再转发给下游。拦截之后通常可以做一点手脚,比如增加一个标记位mPending是否消费过的判断,若消费过则不转发给下游。
mPending.compareAndSet(true, false) 这一行的作用,可以分解为两步原子操作:
- 判断当前值是否等于 true
- 如果是,就把它设置为 false
并且只有在「判断结果为真」的情况下,整个方法才会返回 true,否则返回 false。当mPending为true时,整个判断返回true,并且mPending被设置为false;当mPending为false,整个判断返回false。
-
mPending 本身是一个 AtomicBoolean,它可以保证在多线程环境下对布尔值的读写和比较-设置操作都是原子的,不会被中途打断或竞争条件影响。
-
当你调用 setValue(t) 的时候,会先把 mPending 置为 true,表示「有一个新事件待消费」。
-
在观察者的 onChanged() 回调里,通过 compareAndSet(true, false) 去尝试把它从 true 切换回 false:
-
如果能切换成功(也就是它确实还是 true),说明这是第一次消费这个事件,就会执行 observer.onChanged(t),并返回 true。
-
如果切换失败(说明要么之前已经切换过一次变成 false,要么本来就是 false),compareAndSet 返回 false,就不会再通知观察者,也避免了重复消费。
-
1. 非暂态数据 2. 暂态数据
在数据驱动的 App 界面下,存在两种值:1. 非暂态数据 2. 暂态数据
demo 中用于提示“购物车已满”的数据就是“暂态数据”,这种数据是一次性的,转瞬即逝的,可以消费一次就扔掉。
demo 中购物车中的商品列表就是“非暂态数据”,它的生命周期要比暂态数据长一点,在购物车界面和结算界面存活的期间都应该能被重复消费。
SingleLiveEvent 的设计正是基于对数据的这种分类方法,即暂态数据使用 SingleLiveEvent,非暂态数据使用常规的 LiveData。
这样尘归尘土归土的解决方案是符合现实情况的。将 demo 改造一下:
class MyViewModel : ViewModel() {
// 非暂态购物车列表 LiveData
val selectsListLiveData = MutableLiveData<List<String>>()
// 暂态购物车列表 LiveData
val singleListLiveData = SingleLiveEvent<List<String>>()
// 更新购物车列表,同时更新暂态和非暂态
fun setSelectsList(goods: List<String>) {
selectsListLiveData.value = goods
singleListLiveData.value = goods
}
}
在购物车界面做相应的改动:
class TrolleyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 只观察非暂态购物车列表
myViewModel.singleListLiveData.observe(viewLifecycleOwner) { goods ->
goods.takeIf { it.size >= 2 }?.let {
Toast.makeText(context,"full",Toast.LENGTH_LONG).show()
}
}
}
}
但该方案有局限性,若为 SingleLiveEvent 添加多个观察者,则当第一个观察者消费了数据后,其他观察者就没机会消费了。因为mPending是所有观察者共享的。
解决方案也很简单,为每个中间观察者都持有是否消费过数据的标记位:
open class LiveEvent<T> : MediatorLiveData<T>() {
// 持有多个中间观察者
private val observers = ArraySet<ObserverWrapper<in T>>()
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
observers.find { it.observer === observer }?.let { _ ->
return
}
// 构建中间观察者
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observe(owner, wrapper)
}
@MainThread
override fun observeForever(observer: Observer<in T>) {
observers.find { it.observer === observer }?.let { _ ->
return
}
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observeForever(wrapper)
}
@MainThread
override fun removeObserver(observer: Observer<in T>) {
if (observer is ObserverWrapper && observers.remove(observer)) {
super.removeObserver(observer)
return
}
val iterator = observers.iterator()
while (iterator.hasNext()) {
val wrapper = iterator.next()
if (wrapper.observer == observer) {
iterator.remove()
super.removeObserver(wrapper)
break
}
}
}
@MainThread
override fun setValue(t: T?) {
// 通知所有中间观察者,有新数据
observers.forEach { it.newValue() }
super.setValue(t)
}
// 中间观察者
private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {
// 标记当前观察者是否消费了数据
private var pending = false
override fun onChanged(t: T?) {
// 保证只向下游观察者分发一次数据
if (pending) {
pending = false
observer.onChanged(t)
}
}
fun newValue() {
pending = true
}
}
}
使用例子:
class MyViewModel : ViewModel() {
// LiveEvent 用来发一次性事件
val liveEvent: LiveEvent<String> = LiveEvent()
// 模拟触发事件
fun triggerEvent(value: String) {
liveEvent.value = value
}
}
- LiveEvent 保存一个 String 事件。
- 每次调用 liveEvent.value = ... 时,只有“此刻活跃且 pending 为 true”的观察者会收到一次通知。
在Fragment中注册两个数据观察者:
class MyFragment : Fragment(R.layout.fragment_my) {
private val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 第一个观察者:更新 UI(TextView)
viewModel.liveEvent.observe(viewLifecycleOwner) { data ->
// 只有在 pending=true 时才会进来,并且消费一次
textView1.text = "Observer1 收到: $data"
}
// 第二个观察者:打印日志
viewModel.liveEvent.observe(viewLifecycleOwner) { data ->
Log.d("MyFragment", "Observer2 收到: $data")
}
// 模拟按钮点击触发
button.setOnClickListener {
viewModel.triggerEvent("Hello LiveEvent!")
}
}
}
-
observe(viewLifecycleOwner, …) 确保观察者在 Fragment 的视图生命周期内活跃,销毁时自动解绑。
-
注册两次 observe 虽然底层都被包装成不同的 ObserverWrapper,但两个回调都会各自“消费”一次:
textView1 会显示 "Observer1 收到: Hello LiveEvent!"
Logcat 会输出 Observer2 收到: Hello LiveEvent!
-
如果你再旋转屏幕导致重建,只要在新的视图生命周期里重新绑定,这两位观察者并不会“粘”到旧的事件上(因为 pending 在消费后被清零了)。
3.什么情况下 LiveData 会丢失数据?
先总结,再分析: 在高频数据更新的场景下使用 LiveData.postValue() 时,会造成数据丢失。因为“设值”和“分发值”是分开执行的,之间存在延迟。值先被缓存在变量中,再向主线程抛一个分发值的任务。若在这延迟之间再一次调用 postValue(),则变量中缓存的值被更新,之前的值在没有被分发之前就被擦除了。
下面是 LiveData.postValue() 的源码:
public abstract class LiveData<T> {
// 暂存值字段
volatile Object mPendingData = NOT_SET;
private final Runnable mPostValueRunnable = new Runnable() {
@Override
public void run() {
Object newValue;
synchronized (mDataLock) {
// 同步地获取暂存值
newValue = mPendingData;
mPendingData = NOT_SET;
}
// 分发值
setValue((T) newValue);
}
};
protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
// 暂存值
mPendingData = value;
}
...
// 向主线程抛 runnable
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}
}
接下来的相关内容,参照文章: LiveData使用注意事项