Android jetpack 架构组件复习----LiveData相关的一些问题

207 阅读9分钟

1.LiveData 是粘性的吗?若是,它是怎么做到的?

LiveData粘性的特点:

在 Android 的 Architecture Components 里,LiveData 有一个“粘性”(sticky)的特点,主要表现为:

  1. 保持最新值(缓存特性)

LiveData 会存储最近一次调用 setValue() 或 postValue() 发布的数据。

当有新的观察者(Observer)订阅并进入「活跃」状态时,马上就会收到这条最新的值通知,即便这次订阅发生在数据更新之后。

  1. 跨越「订阅–取消订阅」周期

如果一个 LifecycleOwner(如 Activity/Fragment)因为配置变化(旋转、回收重建)或页面切换而短暂取消订阅(onStop() 后进入非活跃),然后又重新进入活跃状态(onStart()/onResume()),它依然会立刻收到那条“粘”在 LiveData 上的最新值。

  1. 保证 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) 这一行的作用,可以分解为两步原子操作:

  1. 判断当前值是否等于 true
  2. 如果是,就把它设置为 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使用注意事项